blob: 6efd222e74023951637da1e30e299e8f9af859ae [file] [log] [blame]
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 {
logM := io.MultiWriter(os.Stdout, s.logs)
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 != "" {
if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
if err != nil {
fmt.Fprintf(logM, "!!! dodo: Failed to clone repository: %s\n", err)
s.status = &Status{
Commit: nil,
}
return err
}
fmt.Fprintf(logM, "!!! dodo: Successfully cloned repository %s\n", commit.Hash)
s.status = &Status{
Commit: commit,
Commands: []CommandStatus{},
}
} else {
s.status = &Status{
Commands: []CommandStatus{},
}
}
} else {
s.status = &Status{
Commit: nil,
Commands: []CommandStatus{},
}
}
if s.agentMode && s.repoAddr == "" {
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",
})
}
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.Fprintf(logM, "!!! 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
}