blob: 1f44481a008084cac1c214fd662e1d09c67a5af8 [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 {
gioaa6e27a2025-06-29 23:17:54 +040033 l sync.Locker
34 // TODO(gio): randomly generate string
35 runId int
gioe65d9a92025-06-19 09:02:32 +040036 agentMode bool
gio0eaf2712024-04-14 13:08:46 +040037 port int
gio266c04f2024-07-03 14:18:45 +040038 appId string
giob87415c2025-05-08 22:32:11 +040039 service string
gio5155c1a2025-05-21 15:36:21 +040040 id string
gio0eaf2712024-04-14 13:08:46 +040041 ready bool
42 cmd *exec.Cmd
43 repoAddr string
gio2b1157a2024-10-24 08:45:07 +040044 branch string
giofc441e32024-11-11 16:26:14 +040045 rootDir string
gio0eaf2712024-04-14 13:08:46 +040046 signer ssh.Signer
47 appDir string
48 runCommands []Command
49 self string
gioa60f0de2024-07-08 10:49:48 +040050 managerAddr string
gioaa6e27a2025-06-29 23:17:54 +040051 logs *Logger
52 logM io.Writer
gio45c31822024-10-24 10:58:02 +040053 currDir string
gio5155c1a2025-05-21 15:36:21 +040054 status *Status
gio0eaf2712024-04-14 13:08:46 +040055}
56
gioe65d9a92025-06-19 09:02:32 +040057func NewServer(agentMode bool, port int, appId, service, id, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
gioaa6e27a2025-06-29 23:17:54 +040058 logger := NewLogger("0")
59 logM := io.MultiWriter(os.Stdout, logger)
gio0eaf2712024-04-14 13:08:46 +040060 return &Server{
61 l: &sync.Mutex{},
gioaa6e27a2025-06-29 23:17:54 +040062 runId: 0,
gioe65d9a92025-06-19 09:02:32 +040063 agentMode: agentMode,
gio0eaf2712024-04-14 13:08:46 +040064 port: port,
65 ready: false,
gio266c04f2024-07-03 14:18:45 +040066 appId: appId,
giob87415c2025-05-08 22:32:11 +040067 service: service,
gio5155c1a2025-05-21 15:36:21 +040068 id: id,
gio0eaf2712024-04-14 13:08:46 +040069 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040070 branch: branch,
giofc441e32024-11-11 16:26:14 +040071 rootDir: rootDir,
gio0eaf2712024-04-14 13:08:46 +040072 signer: signer,
73 appDir: appDir,
74 runCommands: runCommands,
75 self: self,
gioa60f0de2024-07-08 10:49:48 +040076 managerAddr: manager,
gioaa6e27a2025-06-29 23:17:54 +040077 logs: logger,
78 logM: logM,
gio45c31822024-10-24 10:58:02 +040079 currDir: "",
gio5155c1a2025-05-21 15:36:21 +040080 status: nil,
gio0eaf2712024-04-14 13:08:46 +040081 }
82}
83
84func (s *Server) Start() error {
85 http.HandleFunc("/update", s.handleUpdate)
86 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040087 http.HandleFunc("/logs", s.handleLogs)
gioec744fa2025-05-20 12:47:03 +040088 if s.managerAddr != "" && s.appId != "" {
89 go s.pingManager()
90 }
gio0eaf2712024-04-14 13:08:46 +040091 if err := s.run(); err != nil {
92 return err
93 }
gio0eaf2712024-04-14 13:08:46 +040094 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
95}
96
gio183e8342024-08-20 06:01:24 +040097func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
gioaa6e27a2025-06-29 23:17:54 +040098 if logs, err := s.logs.Contents(); err != nil {
99 http.Error(w, "not ready", http.StatusInternalServerError)
100 } else {
101 fmt.Fprint(w, logs)
102 }
gio183e8342024-08-20 06:01:24 +0400103}
104
gio0eaf2712024-04-14 13:08:46 +0400105func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
106 s.l.Lock()
107 defer s.l.Unlock()
108 if s.ready {
109 fmt.Fprintln(w, "ok")
110 } else {
111 http.Error(w, "not ready", http.StatusInternalServerError)
112 }
113}
114
gioaa6e27a2025-06-29 23:17:54 +0400115func (s *Server) log(tmpl string, args ...any) {
116 contents := fmt.Sprintf(tmpl, args...)
117 fmt.Fprintf(s.logs, "\033[38;5;212;136;141mdodo:\033[0;00m %s\n", contents)
118}
119
gio0eaf2712024-04-14 13:08:46 +0400120func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400121 s.l.Lock()
122 s.ready = false
123 s.l.Unlock()
gioaa6e27a2025-06-29 23:17:54 +0400124 s.log("Reloading service")
gio0eaf2712024-04-14 13:08:46 +0400125 if err := s.run(); err != nil {
126 http.Error(w, err.Error(), http.StatusInternalServerError)
127 return
128 }
129 s.l.Lock()
130 s.ready = true
131 s.l.Unlock()
132}
133
gioe65d9a92025-06-19 09:02:32 +0400134type command struct {
135 cmd string
136 env []string
137}
138
gio0eaf2712024-04-14 13:08:46 +0400139func (s *Server) run() error {
gioe65d9a92025-06-19 09:02:32 +0400140 newDir := s.appDir
141 commands := []command{}
142 if !s.agentMode {
143 var err error
144 newDir, err = os.MkdirTemp(s.appDir, "code-*")
145 if err != nil {
146 return err
gio5155c1a2025-05-21 15:36:21 +0400147 }
gio0eaf2712024-04-14 13:08:46 +0400148 }
gioe65d9a92025-06-19 09:02:32 +0400149 if s.repoAddr != "" {
gioe8402352025-06-24 21:28:25 +0400150 if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
151 commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
152 if err != nil {
gioaa6e27a2025-06-29 23:17:54 +0400153 s.log("Failed to clone repository: %s", err)
gioe8402352025-06-24 21:28:25 +0400154 s.status = &Status{
155 Commit: nil,
156 }
157 return err
gioe65d9a92025-06-19 09:02:32 +0400158 }
gioaa6e27a2025-06-29 23:17:54 +0400159 s.log("Successfully cloned repository %s", commit.Hash)
gioe8402352025-06-24 21:28:25 +0400160 s.status = &Status{
161 Commit: commit,
162 Commands: []CommandStatus{},
163 }
164 } else {
165 s.status = &Status{
166 Commands: []CommandStatus{},
167 }
gioe65d9a92025-06-19 09:02:32 +0400168 }
169 } else {
170 s.status = &Status{
171 Commit: nil,
172 Commands: []CommandStatus{},
173 }
gio5155c1a2025-05-21 15:36:21 +0400174 }
gio89c5b5e2025-07-02 12:15:04 +0400175 if s.status.Commit != nil {
176 s.logs.commitHash = s.status.Commit.Hash
177 } else {
178 s.logs.commitHash = ""
179 }
gio24d6e9a2025-06-24 15:02:29 +0400180 if s.agentMode && s.repoAddr == "" {
gioe65d9a92025-06-19 09:02:32 +0400181 if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
182 commands = append(commands, command{cmd: "git config --global user.name dodo"})
183 s.status.Commands = append(s.status.Commands, CommandStatus{
184 Command: commands[len(commands)-1].cmd,
185 State: "waiting",
186 })
187 commands = append(commands, command{cmd: "git config --global user.email dodo@dodo.cloud"})
188 s.status.Commands = append(s.status.Commands, CommandStatus{
189 Command: commands[len(commands)-1].cmd,
190 State: "waiting",
191 })
192 commands = append(commands, command{cmd: "git init ."})
193 s.status.Commands = append(s.status.Commands, CommandStatus{
194 Command: commands[len(commands)-1].cmd,
195 State: "waiting",
196 })
197 commands = append(commands, command{cmd: "echo \"TODO: Describe project\" > README.md"})
198 s.status.Commands = append(s.status.Commands, CommandStatus{
199 Command: commands[len(commands)-1].cmd,
200 State: "waiting",
201 })
202 commands = append(commands, command{cmd: "git add README.md"})
203 s.status.Commands = append(s.status.Commands, CommandStatus{
204 Command: commands[len(commands)-1].cmd,
205 State: "waiting",
206 })
207 commands = append(commands, command{cmd: "git commit -m \"init\""})
208 s.status.Commands = append(s.status.Commands, CommandStatus{
209 Command: commands[len(commands)-1].cmd,
210 State: "waiting",
211 })
212 }
213 }
gio5155c1a2025-05-21 15:36:21 +0400214 for _, c := range s.runCommands {
gio0eaf2712024-04-14 13:08:46 +0400215 args := []string{c.Bin}
216 args = append(args, c.Args...)
gio5155c1a2025-05-21 15:36:21 +0400217 cmd := strings.Join(args, " ")
gioe65d9a92025-06-19 09:02:32 +0400218 commands = append(commands, command{cmd, c.Env})
gio5155c1a2025-05-21 15:36:21 +0400219 s.status.Commands = append(s.status.Commands, CommandStatus{
220 Command: cmd,
221 State: "waiting",
222 })
223 }
gio5155c1a2025-05-21 15:36:21 +0400224 for i, c := range commands {
225 if i > 0 {
226 s.status.Commands[i-1].State = "success"
227 }
gio0eaf2712024-04-14 13:08:46 +0400228 cmd := &exec.Cmd{
giofc441e32024-11-11 16:26:14 +0400229 Dir: filepath.Join(newDir, s.rootDir),
giobcd25e92025-05-03 19:14:10 +0400230 Path: "/bin/sh",
gioe65d9a92025-06-19 09:02:32 +0400231 Args: []string{"/bin/sh", "-c", c.cmd},
232 Env: append(os.Environ(), c.env...),
gioaa6e27a2025-06-29 23:17:54 +0400233 Stdout: s.logM,
234 Stderr: s.logM,
gio0eaf2712024-04-14 13:08:46 +0400235 }
gioff0ee0f2024-10-15 23:11:54 +0400236 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gioaa6e27a2025-06-29 23:17:54 +0400237 s.log("Running: %s", c)
gio5155c1a2025-05-21 15:36:21 +0400238 s.status.Commands[i].State = "running"
gioe65d9a92025-06-19 09:02:32 +0400239 if i < len(commands)-1 {
gio0eaf2712024-04-14 13:08:46 +0400240 if err := cmd.Run(); err != nil {
241 return err
242 }
243 } else {
gio45c31822024-10-24 10:58:02 +0400244 if s.cmd != nil {
245 // TODO(gio): make this point configurable
246 if err := s.kill(); err != nil {
247 return err
248 }
gioe65d9a92025-06-19 09:02:32 +0400249 if s.currDir != "" && !s.agentMode {
gio5155c1a2025-05-21 15:36:21 +0400250 if err := os.RemoveAll(s.currDir); err != nil {
251 return err
252 }
gio45c31822024-10-24 10:58:02 +0400253 }
254 }
gio0eaf2712024-04-14 13:08:46 +0400255 if err := cmd.Start(); err != nil {
256 return err
257 }
258 s.cmd = cmd
259 }
260 }
gio45c31822024-10-24 10:58:02 +0400261 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400262 return nil
263}
264
265type pingReq struct {
gioaa6e27a2025-06-29 23:17:54 +0400266 Id string `json:"id"`
267 Service string `json:"service"`
268 Address string `json:"address"`
269 Status *Status `json:"status,omitempty"`
270 Logs []LogItem `json:"logs"`
271}
272
273type pingResp struct {
274 Success bool `json:"success"`
275 LogItemsConsumed int `json:"logItemsConsumed"`
276}
277
278func min(a, b int) int {
279 if a < b {
280 return a
281 } else {
282 return b
283 }
gio0eaf2712024-04-14 13:08:46 +0400284}
285
286func (s *Server) pingManager() {
287 defer func() {
288 go func() {
gio89c5b5e2025-07-02 12:15:04 +0400289 time.Sleep(500 * time.Millisecond)
gio0eaf2712024-04-14 13:08:46 +0400290 s.pingManager()
291 }()
292 }()
gioaa6e27a2025-06-29 23:17:54 +0400293 logItems := s.logs.Items()
294 logItems = logItems[:min(100, len(logItems))]
gioa60f0de2024-07-08 10:49:48 +0400295 buf, err := json.Marshal(pingReq{
gio5155c1a2025-05-21 15:36:21 +0400296 Id: s.id,
giob87415c2025-05-08 22:32:11 +0400297 Service: s.service,
298 Address: fmt.Sprintf("http://%s:%d", s.self, s.port),
gio5155c1a2025-05-21 15:36:21 +0400299 Status: s.status,
gioaa6e27a2025-06-29 23:17:54 +0400300 Logs: logItems,
gioa60f0de2024-07-08 10:49:48 +0400301 })
gio0eaf2712024-04-14 13:08:46 +0400302 if err != nil {
303 return
304 }
giob87415c2025-05-08 22:32:11 +0400305 registerWorkerAddr := fmt.Sprintf("%s/api/project/%s/workers", s.managerAddr, s.appId)
306 resp, err := http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
307 if err != nil {
308 fmt.Println(err)
309 } else {
gioaa6e27a2025-06-29 23:17:54 +0400310 var r pingResp
311 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
312 fmt.Printf("%s\n", err)
313 } else if r.Success {
314 s.logs.Trim(r.LogItemsConsumed)
315 }
giob87415c2025-05-08 22:32:11 +0400316 }
gio0eaf2712024-04-14 13:08:46 +0400317}
gio45c31822024-10-24 10:58:02 +0400318
319func (s *Server) kill() error {
320 if s.cmd == nil {
321 return nil
322 }
323
324 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
325 if err != nil {
326 return err
327 }
328 // NOTE(gio): No need to check err as we just killed the process
329 s.cmd.Wait()
330 s.cmd = nil
331 return nil
332}