diff --git a/agent/client/cancel.go b/agent/client/cancel.go new file mode 100644 index 0000000..7796ca0 --- /dev/null +++ b/agent/client/cancel.go @@ -0,0 +1,31 @@ +package client + +import ( + "time" + + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/core-utils/fmt" + "github.com/go-zoox/logger" +) + +func (c *client) Cancel() error { + logger.Debugf("cancel event") + + err := c.sendEvent(&event.Event{ + Type: event.Cancel, + }) + + if err != nil { + return err + } + + timer := time.NewTicker(30 * time.Second) + select { + case <-c.core.Context().Done(): + return c.core.Context().Err() + case <-c.cancelEventDone: + return nil + case <-timer.C: + return fmt.Errorf("timeout to wait start event") + } +} diff --git a/agent/client/client.go b/agent/client/client.go new file mode 100644 index 0000000..0d64298 --- /dev/null +++ b/agent/client/client.go @@ -0,0 +1,91 @@ +package client + +import ( + "io" + "os" + + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/command/terminal" + "github.com/go-zoox/logger" + "github.com/go-zoox/websocket" + + command "github.com/go-zoox/command/config" +) + +type Client interface { + Connect() error + Close() error + // + New(command *command.Config) error + // + Start() error + Wait() error + Cancel() error + // + SetStdin(stdin io.Reader) error + SetStdout(stdout io.Writer) error + SetStderr(stderr io.Writer) error + // + Run() error + // + Terminal() (terminal.Terminal, error) +} + +type client struct { + opt *Option + // + core websocket.Conn + // + stdin io.Reader + stdout io.Writer + stderr io.Writer + // + exitcodeCh chan int + // + newEventDone chan struct{} + startEventDone chan struct{} + waitEventDone chan struct{} + cancelEventDone chan struct{} +} + +type Option struct { + Server string +} + +func New(opts ...func(opt *Option)) (Client, error) { + opt := &Option{ + Server: "ws://localhost:8080", + } + for _, o := range opts { + o(opt) + } + + return &client{ + opt: opt, + // + stdin: os.Stdin, + stdout: os.Stdout, + stderr: os.Stderr, + // + exitcodeCh: make(chan int), + // + newEventDone: make(chan struct{}), + startEventDone: make(chan struct{}), + waitEventDone: make(chan struct{}), + cancelEventDone: make(chan struct{}), + }, nil +} + +func (c *client) sendEvent(evt *event.Event) error { + s, err := evt.Encode() + if err != nil { + return err + } + + logger.Debugf("send event to server: %s", s) + if err := c.core.WriteTextMessage(s); err != nil { + return err + } + + return nil +} diff --git a/agent/client/close.go b/agent/client/close.go new file mode 100644 index 0000000..2672c44 --- /dev/null +++ b/agent/client/close.go @@ -0,0 +1,5 @@ +package client + +func (c *client) Close() error { + return c.core.Close() +} diff --git a/agent/client/connect.go b/agent/client/connect.go new file mode 100644 index 0000000..bdd4c8b --- /dev/null +++ b/agent/client/connect.go @@ -0,0 +1,107 @@ +package client + +import ( + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/core-utils/fmt" + "github.com/go-zoox/logger" + "github.com/go-zoox/websocket" + "github.com/go-zoox/websocket/conn" + "github.com/spf13/cast" +) + +func (c *client) Connect() error { + logger.Debugf("create websocket client to %s", c.opt.Server) + + if c.opt.Server == "" { + return fmt.Errorf("server address is required") + } + + ws, err := websocket.NewClient(func(opt *websocket.ClientOption) { + opt.Addr = c.opt.Server + }) + if err != nil { + return err + } + + connected := make(chan struct{}) + + logger.Debugf("listen on close event ...") + ws.OnClose(func(conn conn.Conn, code int, message string) error { + c.stderr.Write([]byte(message)) + c.exitcodeCh <- code + return nil + }) + + logger.Debugf("listen on connect event ...") + ws.OnConnect(func(cc websocket.Conn) error { + c.core = cc + + connected <- struct{}{} + + cc.OnMessage(func(typ int, message []byte) error { + if typ != conn.TextMessage { + return nil + } + + evt := &event.Event{} + if err := evt.Decode(message); err != nil { + return err + } + + logger.Debugf("receive event from server: %s", message) + + switch evt.Type { + case event.Done: + responseEvent := &event.DoneEvent{} + if err := responseEvent.Decode(message); err != nil { + return err + } + switch string(responseEvent.Payload) { + case event.New: + c.newEventDone <- struct{}{} + case event.Start: + c.startEventDone <- struct{}{} + case event.Wait: + c.waitEventDone <- struct{}{} + case event.Cancel: + c.cancelEventDone <- struct{}{} + } + case event.Stdout: + stdoutEvent := &event.StdoutEvent{} + if err := stdoutEvent.Decode(message); err != nil { + return err + } + + c.stdout.Write(stdoutEvent.Payload) + case event.Stderr: + stderrEvent := &event.StderrEvent{} + if err := stderrEvent.Decode(message); err != nil { + return err + } + + c.stderr.Write(stderrEvent.Payload) + case event.Exitcode: + exitcodeEvent := &event.ExitcodeEvent{} + if err := exitcodeEvent.Decode(message); err != nil { + return err + } + + exitcode := cast.ToInt(string(exitcodeEvent.Payload)) + c.exitcodeCh <- exitcode + } + + return nil + }) + + return nil + }) + + logger.Debugf("connect to server ...") + if err := ws.Connect(); err != nil { + return err + } + + <-connected + logger.Debugf("connected") + return nil +} diff --git a/agent/client/io.go b/agent/client/io.go new file mode 100644 index 0000000..1259103 --- /dev/null +++ b/agent/client/io.go @@ -0,0 +1,18 @@ +package client + +import "io" + +func (c *client) SetStdin(stdin io.Reader) error { + c.stdin = stdin + return nil +} + +func (c *client) SetStdout(stdout io.Writer) error { + c.stdout = stdout + return nil +} + +func (c *client) SetStderr(stderr io.Writer) error { + c.stderr = stderr + return nil +} diff --git a/agent/client/new.go b/agent/client/new.go new file mode 100644 index 0000000..12a5efe --- /dev/null +++ b/agent/client/new.go @@ -0,0 +1,32 @@ +package client + +import ( + "fmt" + "time" + + "github.com/go-zoox/command/agent/event" + command "github.com/go-zoox/command/config" + "github.com/go-zoox/logger" +) + +func (c *client) New(command *command.Config) error { + logger.Debugf("new event with command: %s", command.Command) + + err := c.sendEvent(&event.Event{ + Type: event.New, + Payload: command, + }) + if err != nil { + return err + } + + timer := time.NewTicker(30 * time.Second) + select { + case <-c.core.Context().Done(): + return c.core.Context().Err() + case <-c.newEventDone: + return nil + case <-timer.C: + return fmt.Errorf("timeout to await new event") + } +} diff --git a/agent/client/run.go b/agent/client/run.go new file mode 100644 index 0000000..d950da8 --- /dev/null +++ b/agent/client/run.go @@ -0,0 +1,9 @@ +package client + +func (c *client) Run() error { + if err := c.Start(); err != nil { + return err + } + + return c.Wait() +} diff --git a/agent/client/start.go b/agent/client/start.go new file mode 100644 index 0000000..84dd4e4 --- /dev/null +++ b/agent/client/start.go @@ -0,0 +1,30 @@ +package client + +import ( + "time" + + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/core-utils/fmt" + "github.com/go-zoox/logger" +) + +func (c *client) Start() error { + logger.Debugf("start event") + + err := c.sendEvent(&event.Event{ + Type: event.Start, + }) + if err != nil { + return err + } + + timer := time.NewTicker(30 * time.Second) + select { + case <-c.core.Context().Done(): + return c.core.Context().Err() + case <-c.startEventDone: + return nil + case <-timer.C: + return fmt.Errorf("timeout to wait start event") + } +} diff --git a/agent/client/terminal.go b/agent/client/terminal.go new file mode 100644 index 0000000..87a7385 --- /dev/null +++ b/agent/client/terminal.go @@ -0,0 +1,11 @@ +package client + +import ( + "fmt" + + "github.com/go-zoox/command/terminal" +) + +func (c *client) Terminal() (terminal.Terminal, error) { + return nil, fmt.Errorf("not implemented in agent") +} diff --git a/agent/client/wait.go b/agent/client/wait.go new file mode 100644 index 0000000..c4dda0f --- /dev/null +++ b/agent/client/wait.go @@ -0,0 +1,35 @@ +package client + +import ( + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/command/errors" + "github.com/go-zoox/logger" +) + +func (c *client) Wait() error { + logger.Debugf("wait event") + + err := c.sendEvent(&event.Event{ + Type: event.Wait, + }) + if err != nil { + return err + } + + select { + case <-c.core.Context().Done(): + return c.core.Context().Err() + case <-c.waitEventDone: + logger.Debugf("wait for exit code ...") + code := <-c.exitcodeCh + logger.Debugf("exit code is %d", code) + + if code == 0 { + return nil + } + + return &errors.ExitError{ + Code: code, + } + } +} diff --git a/agent/event/cancel.go b/agent/event/cancel.go new file mode 100644 index 0000000..12e0da5 --- /dev/null +++ b/agent/event/cancel.go @@ -0,0 +1,6 @@ +package event + +const Cancel = "cancel" + +type CancelEvent struct { +} diff --git a/agent/event/done.go b/agent/event/done.go new file mode 100644 index 0000000..e79b86e --- /dev/null +++ b/agent/event/done.go @@ -0,0 +1,17 @@ +package event + +import "encoding/json" + +const Done = "event.done" + +type DoneEvent struct { + Payload []byte +} + +func (se *DoneEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, se) +} + +func (se *DoneEvent) Encode() ([]byte, error) { + return json.Marshal(se) +} diff --git a/agent/event/event.go b/agent/event/event.go new file mode 100644 index 0000000..bd12b7f --- /dev/null +++ b/agent/event/event.go @@ -0,0 +1,16 @@ +package event + +import "encoding/json" + +type Event struct { + Type string + Payload interface{} +} + +func (e *Event) Decode(raw []byte) error { + return json.Unmarshal(raw, e) +} + +func (e *Event) Encode() ([]byte, error) { + return json.Marshal(e) +} diff --git a/agent/event/exitcode.go b/agent/event/exitcode.go new file mode 100644 index 0000000..847e983 --- /dev/null +++ b/agent/event/exitcode.go @@ -0,0 +1,17 @@ +package event + +import "encoding/json" + +const Exitcode = "exitcode" + +type ExitcodeEvent struct { + Payload []byte +} + +func (ee *ExitcodeEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, ee) +} + +func (ee *ExitcodeEvent) Encode() ([]byte, error) { + return json.Marshal(ee) +} diff --git a/agent/event/new.go b/agent/event/new.go new file mode 100644 index 0000000..fbddc96 --- /dev/null +++ b/agent/event/new.go @@ -0,0 +1,21 @@ +package event + +import ( + "encoding/json" + + command "github.com/go-zoox/command/config" +) + +const New = "new" + +type NewEvent struct { + Payload *command.Config +} + +func (ne *NewEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, ne) +} + +func (ne *NewEvent) Encode() ([]byte, error) { + return json.Marshal(ne) +} diff --git a/agent/event/start.go b/agent/event/start.go new file mode 100644 index 0000000..20e3396 --- /dev/null +++ b/agent/event/start.go @@ -0,0 +1,6 @@ +package event + +const Start = "start" + +type StartEvent struct { +} diff --git a/agent/event/stderr.go b/agent/event/stderr.go new file mode 100644 index 0000000..c7e0382 --- /dev/null +++ b/agent/event/stderr.go @@ -0,0 +1,17 @@ +package event + +import "encoding/json" + +const Stderr = "stderr" + +type StderrEvent struct { + Payload []byte +} + +func (se *StderrEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, se) +} + +func (se *StderrEvent) Encode() ([]byte, error) { + return json.Marshal(se) +} diff --git a/agent/event/stdin.go b/agent/event/stdin.go new file mode 100644 index 0000000..e6ada78 --- /dev/null +++ b/agent/event/stdin.go @@ -0,0 +1,17 @@ +package event + +import "encoding/json" + +const Stdin = "stdin" + +type StdinEvent struct { + Payload []byte +} + +func (ne *StdinEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, ne) +} + +func (ne *StdinEvent) Encode() ([]byte, error) { + return json.Marshal(ne) +} diff --git a/agent/event/stdout.go b/agent/event/stdout.go new file mode 100644 index 0000000..8a89808 --- /dev/null +++ b/agent/event/stdout.go @@ -0,0 +1,17 @@ +package event + +import "encoding/json" + +const Stdout = "stdout" + +type StdoutEvent struct { + Payload []byte +} + +func (se *StdoutEvent) Decode(raw []byte) error { + return json.Unmarshal(raw, se) +} + +func (se *StdoutEvent) Encode() ([]byte, error) { + return json.Marshal(se) +} diff --git a/agent/event/wait.go b/agent/event/wait.go new file mode 100644 index 0000000..afb2f92 --- /dev/null +++ b/agent/event/wait.go @@ -0,0 +1,6 @@ +package event + +const Wait = "wait" + +type WaitEvent struct { +} diff --git a/agent/server/run.go b/agent/server/run.go new file mode 100644 index 0000000..f67d0a9 --- /dev/null +++ b/agent/server/run.go @@ -0,0 +1,31 @@ +package server + +import ( + "fmt" + + "github.com/go-zoox/websocket" + "github.com/go-zoox/websocket/conn" +) + +func (s *server) Run() error { + ws, err := websocket.NewServer() + if err != nil { + return err + } + + ws.OnClose(func(conn conn.Conn, code int, message string) error { + return nil + }) + + ws.OnError(func(conn conn.Conn, err error) error { + return nil + }) + + ws.OnConnect(func(conn conn.Conn) error { + go Worker(conn) + + return nil + }) + + return ws.Run(fmt.Sprintf(":%d", s.opt.Port)) +} diff --git a/agent/server/server.go b/agent/server/server.go new file mode 100644 index 0000000..ca91c32 --- /dev/null +++ b/agent/server/server.go @@ -0,0 +1,28 @@ +package server + +type Server interface { + Run() error +} + +type server struct { + opt *Option +} + +type Option struct { + Port int + Path string +} + +func New(opts ...func(opt *Option)) (Server, error) { + opt := &Option{ + Port: 8080, + Path: "/", + } + for _, o := range opts { + o(opt) + } + + return &server{ + opt: opt, + }, nil +} diff --git a/agent/server/worker.go b/agent/server/worker.go new file mode 100644 index 0000000..95cea16 --- /dev/null +++ b/agent/server/worker.go @@ -0,0 +1,184 @@ +package server + +import ( + "encoding/json" + "fmt" + gio "io" + + "github.com/go-zoox/command" + "github.com/go-zoox/command/agent/event" + "github.com/go-zoox/command/errors" + "github.com/go-zoox/core-utils/io" + "github.com/go-zoox/eventemitter" + "github.com/go-zoox/logger" + "github.com/go-zoox/websocket/conn" +) + +func Worker(c conn.Conn) { + // @2 utils + sendEvent := func(evt *event.Event) error { + s, err := json.Marshal(evt) + if err != nil { + return err + } + + return c.WriteTextMessage(s) + } + + createWriter := func(eventName string) gio.Writer { + return io.WriterWrapFunc(func(b []byte) (n int, err error) { + err = sendEvent(&event.Event{ + Type: eventName, + Payload: b, + }) + if err != nil { + return 0, err + } + + return len(b), nil + }) + } + + // @1 vars + eventBus := eventemitter.New(func(opt *eventemitter.Option) { + opt.Context = c.Context() + }) + // + stdout := createWriter(event.Stdout) + stderr := createWriter(event.Stderr) + exitcode := createWriter(event.Exitcode) + // + done := createWriter(event.Done) + // + var cmd command.Command + var cfg *command.Config + + // @3 connection listeners + c.OnMessage(func(typ int, message []byte) error { + if typ != conn.TextMessage { + return nil + } + + evt := &event.Event{} + if err := evt.Decode(message); err != nil { + return err + } + + switch evt.Type { + case event.New: + newEvent := &event.NewEvent{} + if err := newEvent.Decode(message); err != nil { + return err + } + eventBus.Emit(event.New, newEvent.Payload) + case event.Start: + eventBus.Emit(event.Start, nil) + case event.Wait: + eventBus.Emit(event.Wait, nil) + case event.Cancel: + eventBus.Emit(event.Cancel, nil) + // + case event.Stdin: + stdinEvent := &event.StdinEvent{} + if err := stdinEvent.Decode(message); err != nil { + return err + } + + eventBus.Emit(event.Stdin, stdinEvent.Payload) + default: + return fmt.Errorf("unknown event type: %s", evt.Type) + } + + return nil + }) + + // @4 event listeners + eventBus.On("error", eventemitter.HandleFunc(func(payload any) { + err, ok := payload.(error) + if !ok { + return + } + + if errx, ok := err.(*errors.ExitError); ok { + logger.Debugf("failed to run command: %s (exit code: %d)", cfg.Command, errx.ExitCode()) + exitcode.Write([]byte(fmt.Sprintf("%d", errx.ExitCode()))) + return + } + + logger.Debugf("failed to run command(2): %s (exit code: %d)", cfg.Command, 1) + exitcode.Write([]byte("1")) + })) + + eventBus.On(event.New, eventemitter.HandleFunc(func(payload any) { + defer done.Write([]byte(event.New)) + + logger.Debugf("[stage:%s] create command ...", event.New) + if cmd != nil { + eventBus.Emit("error", fmt.Errorf("[stage:%s] command is already created", event.New)) + return + } + + cfg = payload.(*command.Config) + cfg.Context = c.Context() + + cm, err := command.New(cfg) + if err != nil { + eventBus.Emit("error", err) + return + } + cmd = cm + + // cmd.SetStdin(stdin) + cmd.SetStdout(stdout) + cmd.SetStderr(stderr) + })) + + eventBus.On(event.Start, eventemitter.HandleFunc(func(payload any) { + defer done.Write([]byte(event.Start)) + + logger.Debugf("[stage:%s] start command ...", event.Start) + if cmd == nil { + eventBus.Emit("error", fmt.Errorf("[stage:%s] command is not created", event.Start)) + return + } + + if err := cmd.Start(); err != nil { + eventBus.Emit("error", err) + return + } + + })) + + eventBus.On(event.Wait, eventemitter.HandleFunc(func(payload any) { + defer done.Write([]byte(event.Wait)) + + logger.Debugf("[stage:%s] wait for command ...", event.Wait) + if cmd == nil { + eventBus.Emit("error", fmt.Errorf("[stage:%s] command is not created", event.Wait)) + return + } + + if err := cmd.Wait(); err != nil { + eventBus.Emit("error", err) + return + } + + logger.Debugf("[stage:%s] command is done", event.Wait) + exitcode.Write([]byte("0")) + })) + + eventBus.On(event.Cancel, eventemitter.HandleFunc(func(payload any) { + done.Write([]byte(event.Cancel)) + + logger.Debugf("[stage:%s] cancel command ...", event.Cancel) + if cmd == nil { + eventBus.Emit("error", fmt.Errorf("[stage:%s] command is not created", event.Cancel)) + return + } + + if err := cmd.Cancel(); err != nil { + eventBus.Emit("error", err) + return + } + })) +} diff --git a/cmd/agent/client.go b/cmd/agent/client.go new file mode 100644 index 0000000..6f2097b --- /dev/null +++ b/cmd/agent/client.go @@ -0,0 +1,67 @@ +package main + +import ( + "os" + + "github.com/go-zoox/cli" + "github.com/go-zoox/command" + "github.com/go-zoox/command/agent/client" + "github.com/go-zoox/command/errors" +) + +func registerClientCommand(app *cli.MultipleProgram) { + app.Register("client", &cli.Command{ + Name: "client", + Usage: "Run command agent client", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "command", + Usage: "Command to run", + Aliases: []string{"c"}, + Required: true, + }, + &cli.StringFlag{ + Name: "server", + Usage: "Command server address", + Aliases: []string{"s"}, + EnvVars: []string{"SERVER"}, + Value: "ws://localhost:8080", + }, + }, + Action: func(ctx *cli.Context) error { + s, err := client.New(func(opt *client.Option) { + opt.Server = ctx.String("server") + }) + if err != nil { + return err + } + + if err := s.Connect(); err != nil { + return err + } + + err = s.New(&command.Config{ + Command: ctx.String("command"), + // Timeout: 1 * time.Microsecond, + }) + if err != nil { + if errx, ok := err.(*errors.ExitError); ok { + os.Exit(errx.ExitCode()) + return nil + } + + return err + } + + err = s.Run() + if err != nil { + if errx, ok := err.(*errors.ExitError); ok { + os.Exit(errx.ExitCode()) + return nil + } + } + + return err + }, + }) +} diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..4d851fd --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/go-zoox/cli" + "github.com/go-zoox/command" +) + +func main() { + app := cli.NewMultipleProgram(&cli.MultipleProgramConfig{ + Name: "command-runner", + Usage: "Powerful command runner", + Version: command.Version, + }) + + registerServerCommand(app) + registerClientCommand(app) + + app.Run() +} diff --git a/cmd/agent/server.go b/cmd/agent/server.go new file mode 100644 index 0000000..cdd2af5 --- /dev/null +++ b/cmd/agent/server.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/go-zoox/cli" + "github.com/go-zoox/command/agent/server" +) + +func registerServerCommand(app *cli.MultipleProgram) { + app.Register("server", &cli.Command{ + Name: "server", + Usage: "Run command server", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "port", + Usage: "Command server port", + Aliases: []string{"p"}, + EnvVars: []string{"PORT"}, + Value: 8080, + }, + }, + Action: func(ctx *cli.Context) error { + s, err := server.New(func(opt *server.Option) { + opt.Port = ctx.Int("port") + }) + if err != nil { + return err + } + + return s.Run() + }, + }) +} diff --git a/cmd/command-runner/commands/exec.go b/cmd/cmd/commands/exec.go similarity index 98% rename from cmd/command-runner/commands/exec.go rename to cmd/cmd/commands/exec.go index 13996de..e1f32f5 100644 --- a/cmd/command-runner/commands/exec.go +++ b/cmd/cmd/commands/exec.go @@ -23,6 +23,11 @@ func Exec(app *cli.MultipleProgram) { Name: "exec", Usage: "command execute", Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "agent", + Usage: "command agent server", + EnvVars: []string{"AGENT"}, + }, &cli.StringFlag{ Name: "engine", Usage: "command engine, avaliable: host, docker, caas", @@ -155,6 +160,8 @@ func Exec(app *cli.MultipleProgram) { }, Action: func(ctx *cli.Context) (err error) { cmd, err := command.New(&command.Config{ + Agent: ctx.String("agent"), + // Engine: ctx.String("engine"), Command: ctx.String("command"), WorkDir: ctx.String("workdir"), diff --git a/cmd/command-runner/main.go b/cmd/cmd/main.go similarity index 83% rename from cmd/command-runner/main.go rename to cmd/cmd/main.go index 7d1ad3b..79cebfb 100644 --- a/cmd/command-runner/main.go +++ b/cmd/cmd/main.go @@ -3,7 +3,7 @@ package main import ( "github.com/go-zoox/cli" "github.com/go-zoox/command" - "github.com/go-zoox/command/cmd/command-runner/commands" + "github.com/go-zoox/command/cmd/cmd/commands" ) func main() { diff --git a/command.go b/command.go index 178e701..776b56f 100644 --- a/command.go +++ b/command.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + "github.com/go-zoox/command/agent/client" "github.com/go-zoox/command/config" "github.com/go-zoox/command/engine" "github.com/go-zoox/command/engine/caas" @@ -70,6 +71,58 @@ func New(cfg *Config) (cmd Command, err error) { environment[k] = v } + // support agent + if cfg.Agent != "" { + agent, err := client.New(func(opt *client.Option) { + opt.Server = cfg.Agent + }) + if err != nil { + return nil, err + } + + if err := agent.Connect(); err != nil { + return nil, err + } + + err = agent.New(&config.Config{ + // Context: cfg.Context, + Timeout: cfg.Timeout, + Engine: cfg.Engine, + Command: cfg.Command, + WorkDir: cfg.WorkDir, + Environment: environment, + User: cfg.User, + Shell: cfg.Shell, + ReadOnly: cfg.ReadOnly, + IsHistoryDisabled: cfg.IsHistoryDisabled, + Image: cfg.Image, + Memory: cfg.Memory, + CPU: cfg.CPU, + Platform: cfg.Platform, + Network: cfg.Network, + DisableNetwork: cfg.DisableNetwork, + Privileged: cfg.Privileged, + DockerHost: cfg.DockerHost, + Server: cfg.Server, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + SSHHost: cfg.SSHHost, + SSHPort: cfg.SSHPort, + SSHUser: cfg.SSHUser, + SSHPass: cfg.SSHPass, + SSHPrivateKey: cfg.SSHPrivateKey, + SSHPrivateKeySecret: cfg.SSHPrivateKeySecret, + SSHIsIgnoreStrictHostKeyChecking: cfg.SSHIsIgnoreStrictHostKeyChecking, + SSHKnowHostsFilePath: cfg.SSHKnowHostsFilePath, + ID: cfg.ID, + }) + if err != nil { + return nil, err + } + + return agent, nil + } + var engine engine.Engine switch cfg.Engine { case host.Name: diff --git a/config/config.go b/config/config.go index d19301e..dbf9728 100644 --- a/config/config.go +++ b/config/config.go @@ -64,4 +64,7 @@ type Config struct { // Custom Command Runner ID ID string + + // Agent is the command runner agent address + Agent string } diff --git a/go.mod b/go.mod index 4a3ca58..02de774 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,18 @@ require ( github.com/docker/cli v24.0.6+incompatible github.com/docker/docker v24.0.6+incompatible github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 - github.com/go-zoox/cli v1.3.6 + github.com/go-zoox/cli v1.3.8 github.com/go-zoox/commands-as-a-service v1.6.1 - github.com/go-zoox/core-utils v1.3.2 + github.com/go-zoox/core-utils v1.3.6 + github.com/go-zoox/eventemitter v1.3.3 github.com/go-zoox/fs v1.3.13 github.com/go-zoox/logger v1.4.6 github.com/go-zoox/uuid v0.0.1 + github.com/go-zoox/websocket v1.0.0 github.com/opencontainers/image-spec v1.0.2 github.com/spf13/cast v1.5.1 - golang.org/x/crypto v0.13.0 - golang.org/x/term v0.12.0 + golang.org/x/crypto v0.14.0 + golang.org/x/term v0.13.0 ) require ( @@ -33,14 +35,16 @@ require ( github.com/go-zoox/chalk v1.0.2 // indirect github.com/go-zoox/config v1.2.10 // indirect github.com/go-zoox/datetime v1.2.2 // indirect - github.com/go-zoox/dotenv v1.2.3 // indirect + github.com/go-zoox/dotenv v1.2.5 // indirect github.com/go-zoox/encoding v1.2.1 // indirect + github.com/go-zoox/errors v1.0.2 // indirect github.com/go-zoox/ini v1.0.4 // indirect - github.com/go-zoox/tag v1.2.3 // indirect + github.com/go-zoox/safe v1.0.1 // indirect + github.com/go-zoox/tag v1.2.6 // indirect github.com/goccy/go-yaml v1.11.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.3.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -57,10 +61,14 @@ require ( github.com/urfave/cli/v2 v2.25.7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gotest.tools/v3 v3.5.1 // indirect ) + +// replace github.com/go-zoox/websocket => ../websocket + +// replace github.com/go-zoox/eventemitter => ../eventemitter diff --git a/go.sum b/go.sum index 320a41e..c82e988 100644 --- a/go.sum +++ b/go.sum @@ -38,31 +38,39 @@ github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-zoox/chalk v1.0.2 h1:DCWft37fogmvqF37JdbGSLg28L/tQeA8u0lMvb62KOg= github.com/go-zoox/chalk v1.0.2/go.mod h1:z5+qvE9nEJI5uT4px2tyoFa/xxkqf3CUo22KmXLKbNI= -github.com/go-zoox/cli v1.3.6 h1:+A4l9aBBvqO/xXjwZ1PCcX0ue/HZ/Ft0Wp/dDq27iVE= -github.com/go-zoox/cli v1.3.6/go.mod h1:25ox3mVRJdRSyLJLvK8OHYEuXtMtfkTseWkJjlK9kM4= +github.com/go-zoox/cli v1.3.8 h1:YZFsIewCm3wgDxrnXMa67FS+RIX63G2TVMCuq5RJGFk= +github.com/go-zoox/cli v1.3.8/go.mod h1:N7zagYCm0savR4dHKaTficmqkMs/REal2mjgkEpiY/o= github.com/go-zoox/commands-as-a-service v1.6.1 h1:XJ7nQkF3jJt6x0jhTfISaLAXcmzIgUm+6eCB/d336bI= github.com/go-zoox/commands-as-a-service v1.6.1/go.mod h1:hcv7blqvdudBnGAGdKDfy0WMo+Ahg6eZn5W0bwoCcLk= github.com/go-zoox/config v1.2.10 h1:mebuz6O0N81OXOCwtV+LKOiFuAfZ5wyaGsuzkGSSpf4= github.com/go-zoox/config v1.2.10/go.mod h1:KnSEhz7AVMqQfznJzgpzFhQfgy8UZYeone/osbvnVUE= -github.com/go-zoox/core-utils v1.3.2 h1:DFd540Vcus6Z4O4Vi5tXZREU8uEJ7JMoGjvpoZ7VBy8= -github.com/go-zoox/core-utils v1.3.2/go.mod h1:raOOwr2l2sJQyjR0Dg33sg0ry4U1/L2eNTuLFRpUXWs= +github.com/go-zoox/core-utils v1.3.6 h1:p7d3BenyLr/SN4J3L+tr1zdSSChQt3CM9iCHgM0MdGc= +github.com/go-zoox/core-utils v1.3.6/go.mod h1:raOOwr2l2sJQyjR0Dg33sg0ry4U1/L2eNTuLFRpUXWs= github.com/go-zoox/datetime v1.2.2 h1:JrI4ekdsvpsenGzrNQAOmobBTYyotaXD3YDXngvCbM4= github.com/go-zoox/datetime v1.2.2/go.mod h1:qvaCrzjhq/g/gstx4sx06Nl4ll2pLSrkRa9ueLjrZ9A= -github.com/go-zoox/dotenv v1.2.3 h1:9wx4sL2u/FrRLkzoOb7ozYii6NoGsl05KoGdZm1ebIE= -github.com/go-zoox/dotenv v1.2.3/go.mod h1:hhl5WZUxI+DNJN6Zos1y7Nq0jDCXciphswPSYtInqg0= +github.com/go-zoox/dotenv v1.2.5 h1:zCWYvy7zARJDR6kA9CcaYCfCx8zb3Cb5wem2MikZ8lY= +github.com/go-zoox/dotenv v1.2.5/go.mod h1:dWAYv/KrzLZDyo2ESShVHVysqcr3cjAR9vSMsL9U8Nk= github.com/go-zoox/encoding v1.2.1 h1:38rQRsfL1f1YHZaqsPaGcNMkPnzatnPlYiHioUh9F4A= github.com/go-zoox/encoding v1.2.1/go.mod h1:NdcM7Ln73oVW3vJgx3MH4fJknCcdQfq+NgJ0tuCo7tU= +github.com/go-zoox/errors v1.0.2 h1:1NLMoEVlDU1+qrvvPj+rrJXOvQPdeZ3DekVBFrI5PFY= +github.com/go-zoox/errors v1.0.2/go.mod h1:HJ5NKQb9cu3IbI0Jayw7xZiblLBEIglpaIOMxvQnWnk= +github.com/go-zoox/eventemitter v1.3.3 h1:yoqb27z8fwr7oSjCnFmPA01XyfC2dO3YpMpv3E7hmH4= +github.com/go-zoox/eventemitter v1.3.3/go.mod h1:XgyFxOZyk4RKhmFN6FVzkFbA6Lw9khGSN8BjPB909dA= github.com/go-zoox/fs v1.3.13 h1:fe0uvtXCM+9s51z/CnQ5kxB4hBYaK55tkrE9gq0385U= github.com/go-zoox/fs v1.3.13/go.mod h1:wCM+UQkjFTxNjOOCNlGcN3k9FeXXUwn9bFnpyjOn55c= github.com/go-zoox/ini v1.0.4 h1:N4mUbAO0juYIRrv3ysjKtpEn/+yQv57eQietsgpkAYQ= github.com/go-zoox/ini v1.0.4/go.mod h1:SisQneNLb1EBeZ5bA5GnrJd8FNg372hQrPh+gb3IzV4= github.com/go-zoox/logger v1.4.6 h1:zHUaB6KQ9rD/N3hM0JJ3/JCNdgtedf4mVBBNNSyWCOg= github.com/go-zoox/logger v1.4.6/go.mod h1:o7ddvv/gMoMa0TomPhHoIz11ZWRbQ92pF6rwYbOY3iQ= -github.com/go-zoox/tag v1.2.3 h1:HDQpRu8rA1xSJt6c+v0O7TfzTjPq5aDtyzW/15aTh94= -github.com/go-zoox/tag v1.2.3/go.mod h1:z9z4iZb/XPE4HwTXJgPIdwgH90c2NysGxIMq9tW+GuU= +github.com/go-zoox/safe v1.0.1 h1:JwWK7xCyv7eyzBbwzQvhK/Ajm8gG2Q9Cvd/KXpdF2zI= +github.com/go-zoox/safe v1.0.1/go.mod h1:lT0iEBmpoia7xfh2bWzdurzdv++4QRUAfeEzIhCnpoA= +github.com/go-zoox/tag v1.2.6 h1:TtqqB45XWupwHasEA+O+lhMsFbKOpXscebdd/CtPsPY= +github.com/go-zoox/tag v1.2.6/go.mod h1:r8rpj8j4cFe2d79oXjbEVgy9bVAWUvvgRblYo8tyhig= github.com/go-zoox/testify v1.0.2 h1:G5sQ3xm0uwCuytnMhgnqZ5BItCt2DN3n2wLBqlIJEWA= github.com/go-zoox/uuid v0.0.1 h1:txqmDavRTq68gzzqWfJQLorFyUp9a7M2lmq2KcwPGPA= github.com/go-zoox/uuid v0.0.1/go.mod h1:0/F4LdfLqFdyqOf7aXoiYXRkXHU324JQ5DZEytXYBPM= +github.com/go-zoox/websocket v1.0.0 h1:Ftu2eszoMlif25mHzJ0ZBfsNJc2bmItGD0Zh8S1Jn9c= +github.com/go-zoox/websocket v1.0.0/go.mod h1:qW6/2pTGULPjqB8z0ZGE7uJ3jGN7KFAGwKVsDLIhG/A= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -70,8 +78,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= @@ -135,8 +143,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= @@ -146,8 +154,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -163,11 +171,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/run.go b/run.go index c9076d9..6e7b7dd 100644 --- a/run.go +++ b/run.go @@ -18,11 +18,12 @@ func (c *command) Run() error { done <- c.Wait() }() - timeout := time.After(c.cfg.Timeout) select { - case <-timeout: + case <-c.cfg.Context.Done(): + return c.cfg.Context.Err() + case <-time.After(c.cfg.Timeout): c.Cancel() - return fmt.Errorf("timeout to run command (timeout: %s)", c.cfg.Timeout) + return fmt.Errorf("timeout to run command (command: %s, timeout: %s)", c.cfg.Command, c.cfg.Timeout) case err := <-done: return err }