| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "sync" |
| "syscall" |
| "time" |
| |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| type CommandState string |
| |
| type CommandStatus struct { |
| Command string `json:"command"` |
| State CommandState `json:"state"` |
| } |
| |
| type Status struct { |
| Commit *Commit `json:"commit"` |
| Commands []CommandStatus `json:"commands"` |
| } |
| |
| type Server struct { |
| l sync.Locker |
| agentMode bool |
| port int |
| appId string |
| service string |
| id string |
| ready bool |
| cmd *exec.Cmd |
| repoAddr string |
| branch string |
| rootDir string |
| signer ssh.Signer |
| appDir string |
| runCommands []Command |
| self string |
| managerAddr string |
| logs *Log |
| currDir string |
| status *Status |
| } |
| |
| func NewServer(agentMode bool, port int, appId, service, id, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server { |
| return &Server{ |
| l: &sync.Mutex{}, |
| agentMode: agentMode, |
| port: port, |
| ready: false, |
| appId: appId, |
| service: service, |
| id: id, |
| repoAddr: repoAddr, |
| branch: branch, |
| rootDir: rootDir, |
| signer: signer, |
| appDir: appDir, |
| runCommands: runCommands, |
| self: self, |
| managerAddr: manager, |
| logs: &Log{}, |
| currDir: "", |
| status: nil, |
| } |
| } |
| |
| func (s *Server) Start() error { |
| http.HandleFunc("/update", s.handleUpdate) |
| http.HandleFunc("/ready", s.handleReady) |
| http.HandleFunc("/logs", s.handleLogs) |
| if s.managerAddr != "" && s.appId != "" { |
| go s.pingManager() |
| } |
| if err := s.run(); err != nil { |
| return err |
| } |
| return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil) |
| } |
| |
| func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprint(w, s.logs.Contents()) |
| } |
| |
| func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) { |
| s.l.Lock() |
| defer s.l.Unlock() |
| if s.ready { |
| fmt.Fprintln(w, "ok") |
| } else { |
| http.Error(w, "not ready", http.StatusInternalServerError) |
| } |
| } |
| |
| func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) { |
| fmt.Println("update") |
| s.l.Lock() |
| s.ready = false |
| s.l.Unlock() |
| fmt.Fprintf(s.logs, "!!! dodo: Reloading service\n") |
| if err := s.run(); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| s.l.Lock() |
| s.ready = true |
| s.l.Unlock() |
| } |
| |
| type command struct { |
| cmd string |
| env []string |
| } |
| |
| func (s *Server) run() error { |
| newDir := s.appDir |
| commands := []command{} |
| if !s.agentMode { |
| var err error |
| newDir, err = os.MkdirTemp(s.appDir, "code-*") |
| if err != nil { |
| return err |
| } |
| } |
| if s.repoAddr != "" { |
| commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir) |
| if err != nil { |
| fmt.Fprintf(s.logs, "!!! dodo: Failed to clone repository\n") |
| s.status = &Status{ |
| Commit: nil, |
| } |
| return err |
| } |
| fmt.Fprintf(s.logs, "!!! dodo: Successfully cloned repository %s\n", commit.Hash) |
| s.status = &Status{ |
| Commit: commit, |
| Commands: []CommandStatus{}, |
| } |
| } else { |
| s.status = &Status{ |
| Commit: nil, |
| Commands: []CommandStatus{}, |
| } |
| } |
| if s.agentMode { |
| if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) { |
| commands = append(commands, command{cmd: "git config --global user.name dodo"}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| commands = append(commands, command{cmd: "git config --global user.email dodo@dodo.cloud"}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| commands = append(commands, command{cmd: "git init ."}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| commands = append(commands, command{cmd: "echo \"TODO: Describe project\" > README.md"}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| commands = append(commands, command{cmd: "git add README.md"}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| commands = append(commands, command{cmd: "git commit -m \"init\""}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: commands[len(commands)-1].cmd, |
| State: "waiting", |
| }) |
| } |
| } |
| for _, c := range s.runCommands { |
| args := []string{c.Bin} |
| args = append(args, c.Args...) |
| cmd := strings.Join(args, " ") |
| commands = append(commands, command{cmd, c.Env}) |
| s.status.Commands = append(s.status.Commands, CommandStatus{ |
| Command: cmd, |
| State: "waiting", |
| }) |
| } |
| logM := io.MultiWriter(os.Stdout, s.logs) |
| for i, c := range commands { |
| if i > 0 { |
| s.status.Commands[i-1].State = "success" |
| } |
| cmd := &exec.Cmd{ |
| Dir: filepath.Join(newDir, s.rootDir), |
| Path: "/bin/sh", |
| Args: []string{"/bin/sh", "-c", c.cmd}, |
| Env: append(os.Environ(), c.env...), |
| Stdout: logM, |
| Stderr: logM, |
| } |
| cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} |
| fmt.Printf("Running: %s\n", c) |
| fmt.Fprintf(s.logs, "!!! dodo: Running: %s\n", c) |
| s.status.Commands[i].State = "running" |
| if i < len(commands)-1 { |
| if err := cmd.Run(); err != nil { |
| return err |
| } |
| } else { |
| if s.cmd != nil { |
| // TODO(gio): make this point configurable |
| if err := s.kill(); err != nil { |
| return err |
| } |
| if s.currDir != "" && !s.agentMode { |
| if err := os.RemoveAll(s.currDir); err != nil { |
| return err |
| } |
| } |
| } |
| if err := cmd.Start(); err != nil { |
| return err |
| } |
| s.cmd = cmd |
| } |
| } |
| s.currDir = newDir |
| return nil |
| } |
| |
| type pingReq struct { |
| Id string `json:"id"` |
| Service string `json:"service"` |
| Address string `json:"address"` |
| Status *Status `json:"status,omitempty"` |
| Logs string `json:"logs"` |
| } |
| |
| func (s *Server) pingManager() { |
| defer func() { |
| go func() { |
| time.Sleep(5 * time.Second) |
| s.pingManager() |
| }() |
| }() |
| buf, err := json.Marshal(pingReq{ |
| Id: s.id, |
| Service: s.service, |
| Address: fmt.Sprintf("http://%s:%d", s.self, s.port), |
| Status: s.status, |
| Logs: s.logs.Contents(), |
| }) |
| if err != nil { |
| return |
| } |
| registerWorkerAddr := fmt.Sprintf("%s/api/project/%s/workers", s.managerAddr, s.appId) |
| resp, err := http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf)) |
| if err != nil { |
| fmt.Println(err) |
| } else { |
| // check resp code |
| io.Copy(os.Stdout, resp.Body) |
| } |
| } |
| |
| func (s *Server) kill() error { |
| if s.cmd == nil { |
| return nil |
| } |
| |
| err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL) |
| if err != nil { |
| return err |
| } |
| // NOTE(gio): No need to check err as we just killed the process |
| s.cmd.Wait() |
| s.cmd = nil |
| return nil |
| } |