blob: 1f7fe01fad34026a289b88ba337e074ae88516b5 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
gio183e8342024-08-20 06:01:24 +04007 "io"
gio0eaf2712024-04-14 13:08:46 +04008 "net/http"
9 "os"
10 "os/exec"
giofc441e32024-11-11 16:26:14 +040011 "path/filepath"
giobcd25e92025-05-03 19:14:10 +040012 "strings"
gio0eaf2712024-04-14 13:08:46 +040013 "sync"
gioff0ee0f2024-10-15 23:11:54 +040014 "syscall"
gio0eaf2712024-04-14 13:08:46 +040015 "time"
16
17 "golang.org/x/crypto/ssh"
18)
19
gio5155c1a2025-05-21 15:36:21 +040020type CommandState string
21
22type CommandStatus struct {
23 Command string `json:"command"`
24 State CommandState `json:"state"`
25}
26
27type Status struct {
gio9635ccb2025-05-22 08:33:38 +040028 Commit *Commit `json:"commit"`
gio5155c1a2025-05-21 15:36:21 +040029 Commands []CommandStatus `json:"commands"`
30}
31
gio0eaf2712024-04-14 13:08:46 +040032type Server struct {
33 l sync.Locker
34 port int
gio266c04f2024-07-03 14:18:45 +040035 appId string
giob87415c2025-05-08 22:32:11 +040036 service string
gio5155c1a2025-05-21 15:36:21 +040037 id string
gio0eaf2712024-04-14 13:08:46 +040038 ready bool
39 cmd *exec.Cmd
40 repoAddr string
gio2b1157a2024-10-24 08:45:07 +040041 branch string
giofc441e32024-11-11 16:26:14 +040042 rootDir string
gio0eaf2712024-04-14 13:08:46 +040043 signer ssh.Signer
44 appDir string
45 runCommands []Command
46 self string
gioa60f0de2024-07-08 10:49:48 +040047 managerAddr string
gio183e8342024-08-20 06:01:24 +040048 logs *Log
gio45c31822024-10-24 10:58:02 +040049 currDir string
gio5155c1a2025-05-21 15:36:21 +040050 status *Status
gio0eaf2712024-04-14 13:08:46 +040051}
52
gio5155c1a2025-05-21 15:36:21 +040053func NewServer(port int, appId, service, id, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
gio0eaf2712024-04-14 13:08:46 +040054 return &Server{
55 l: &sync.Mutex{},
56 port: port,
57 ready: false,
gio266c04f2024-07-03 14:18:45 +040058 appId: appId,
giob87415c2025-05-08 22:32:11 +040059 service: service,
gio5155c1a2025-05-21 15:36:21 +040060 id: id,
gio0eaf2712024-04-14 13:08:46 +040061 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040062 branch: branch,
giofc441e32024-11-11 16:26:14 +040063 rootDir: rootDir,
gio0eaf2712024-04-14 13:08:46 +040064 signer: signer,
65 appDir: appDir,
66 runCommands: runCommands,
67 self: self,
gioa60f0de2024-07-08 10:49:48 +040068 managerAddr: manager,
gio183e8342024-08-20 06:01:24 +040069 logs: &Log{},
gio45c31822024-10-24 10:58:02 +040070 currDir: "",
gio5155c1a2025-05-21 15:36:21 +040071 status: nil,
gio0eaf2712024-04-14 13:08:46 +040072 }
73}
74
75func (s *Server) Start() error {
76 http.HandleFunc("/update", s.handleUpdate)
77 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040078 http.HandleFunc("/logs", s.handleLogs)
gioec744fa2025-05-20 12:47:03 +040079 if s.managerAddr != "" && s.appId != "" {
80 go s.pingManager()
81 }
gio0eaf2712024-04-14 13:08:46 +040082 if err := s.run(); err != nil {
83 return err
84 }
gio0eaf2712024-04-14 13:08:46 +040085 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
86}
87
gio183e8342024-08-20 06:01:24 +040088func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
89 fmt.Fprint(w, s.logs.Contents())
90}
91
gio0eaf2712024-04-14 13:08:46 +040092func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
93 s.l.Lock()
94 defer s.l.Unlock()
95 if s.ready {
96 fmt.Fprintln(w, "ok")
97 } else {
98 http.Error(w, "not ready", http.StatusInternalServerError)
99 }
100}
101
102func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
103 fmt.Println("update")
104 s.l.Lock()
105 s.ready = false
106 s.l.Unlock()
gio4bfed002025-05-22 13:23:22 +0400107 fmt.Fprintf(s.logs, "!!! dodo: Reloading service\n")
gio0eaf2712024-04-14 13:08:46 +0400108 if err := s.run(); err != nil {
109 http.Error(w, err.Error(), http.StatusInternalServerError)
110 return
111 }
112 s.l.Lock()
113 s.ready = true
114 s.l.Unlock()
115}
116
117func (s *Server) run() error {
gio45c31822024-10-24 10:58:02 +0400118 newDir, err := os.MkdirTemp(s.appDir, "code-*")
119 if err != nil {
120 return err
121 }
gio5155c1a2025-05-21 15:36:21 +0400122 commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
123 if err != nil {
gio4bfed002025-05-22 13:23:22 +0400124 fmt.Fprintf(s.logs, "!!! dodo: Failed to clone repository\n")
gio5155c1a2025-05-21 15:36:21 +0400125 s.status = &Status{
gio9635ccb2025-05-22 08:33:38 +0400126 Commit: nil,
gio5155c1a2025-05-21 15:36:21 +0400127 }
gio0eaf2712024-04-14 13:08:46 +0400128 return err
129 }
gio4bfed002025-05-22 13:23:22 +0400130 fmt.Fprintf(s.logs, "!!! dodo: Successfully cloned repository %s\n", commit.Hash)
gio5155c1a2025-05-21 15:36:21 +0400131 s.status = &Status{
gio5155c1a2025-05-21 15:36:21 +0400132 Commit: commit,
133 Commands: []CommandStatus{},
134 }
135 commands := []string{}
136 for _, c := range s.runCommands {
gio0eaf2712024-04-14 13:08:46 +0400137 args := []string{c.Bin}
138 args = append(args, c.Args...)
gio5155c1a2025-05-21 15:36:21 +0400139 cmd := strings.Join(args, " ")
140 commands = append(commands, cmd)
141 s.status.Commands = append(s.status.Commands, CommandStatus{
142 Command: cmd,
143 State: "waiting",
144 })
145 }
146 logM := io.MultiWriter(os.Stdout, s.logs)
147 for i, c := range commands {
148 if i > 0 {
149 s.status.Commands[i-1].State = "success"
150 }
gio0eaf2712024-04-14 13:08:46 +0400151 cmd := &exec.Cmd{
giofc441e32024-11-11 16:26:14 +0400152 Dir: filepath.Join(newDir, s.rootDir),
giobcd25e92025-05-03 19:14:10 +0400153 Path: "/bin/sh",
gio5155c1a2025-05-21 15:36:21 +0400154 Args: []string{"/bin/sh", "-c", c},
155 Env: append(os.Environ(), s.runCommands[i].Env...),
gio183e8342024-08-20 06:01:24 +0400156 Stdout: logM,
157 Stderr: logM,
gio0eaf2712024-04-14 13:08:46 +0400158 }
gioff0ee0f2024-10-15 23:11:54 +0400159 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gio5155c1a2025-05-21 15:36:21 +0400160 fmt.Printf("Running: %s\n", c)
gio4bfed002025-05-22 13:23:22 +0400161 fmt.Fprintf(s.logs, "!!! dodo: Running: %s\n", c)
gio5155c1a2025-05-21 15:36:21 +0400162 s.status.Commands[i].State = "running"
gio0eaf2712024-04-14 13:08:46 +0400163 if i < len(s.runCommands)-1 {
164 if err := cmd.Run(); err != nil {
165 return err
166 }
167 } else {
gio45c31822024-10-24 10:58:02 +0400168 if s.cmd != nil {
169 // TODO(gio): make this point configurable
170 if err := s.kill(); err != nil {
171 return err
172 }
gio5155c1a2025-05-21 15:36:21 +0400173 if s.currDir != "" {
174 if err := os.RemoveAll(s.currDir); err != nil {
175 return err
176 }
gio45c31822024-10-24 10:58:02 +0400177 }
178 }
gio0eaf2712024-04-14 13:08:46 +0400179 if err := cmd.Start(); err != nil {
180 return err
181 }
182 s.cmd = cmd
183 }
184 }
gio45c31822024-10-24 10:58:02 +0400185 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400186 return nil
187}
188
189type pingReq struct {
gio5155c1a2025-05-21 15:36:21 +0400190 Id string `json:"id"`
191 Service string `json:"service"`
192 Address string `json:"address"`
193 Status *Status `json:"status,omitempty"`
194 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400195}
196
197func (s *Server) pingManager() {
198 defer func() {
199 go func() {
200 time.Sleep(5 * time.Second)
201 s.pingManager()
202 }()
203 }()
gioa60f0de2024-07-08 10:49:48 +0400204 buf, err := json.Marshal(pingReq{
gio5155c1a2025-05-21 15:36:21 +0400205 Id: s.id,
giob87415c2025-05-08 22:32:11 +0400206 Service: s.service,
207 Address: fmt.Sprintf("http://%s:%d", s.self, s.port),
gio5155c1a2025-05-21 15:36:21 +0400208 Status: s.status,
gio183e8342024-08-20 06:01:24 +0400209 Logs: s.logs.Contents(),
gioa60f0de2024-07-08 10:49:48 +0400210 })
gio0eaf2712024-04-14 13:08:46 +0400211 if err != nil {
212 return
213 }
giob87415c2025-05-08 22:32:11 +0400214 registerWorkerAddr := fmt.Sprintf("%s/api/project/%s/workers", s.managerAddr, s.appId)
215 resp, err := http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
216 if err != nil {
217 fmt.Println(err)
218 } else {
219 // check resp code
220 io.Copy(os.Stdout, resp.Body)
221 }
gio0eaf2712024-04-14 13:08:46 +0400222}
gio45c31822024-10-24 10:58:02 +0400223
224func (s *Server) kill() error {
225 if s.cmd == nil {
226 return nil
227 }
228
229 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
230 if err != nil {
231 return err
232 }
233 // NOTE(gio): No need to check err as we just killed the process
234 s.cmd.Wait()
235 s.cmd = nil
236 return nil
237}