blob: 6efd222e74023951637da1e30e299e8f9af859ae [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
gioe65d9a92025-06-19 09:02:32 +040034 agentMode bool
gio0eaf2712024-04-14 13:08:46 +040035 port int
gio266c04f2024-07-03 14:18:45 +040036 appId string
giob87415c2025-05-08 22:32:11 +040037 service string
gio5155c1a2025-05-21 15:36:21 +040038 id string
gio0eaf2712024-04-14 13:08:46 +040039 ready bool
40 cmd *exec.Cmd
41 repoAddr string
gio2b1157a2024-10-24 08:45:07 +040042 branch string
giofc441e32024-11-11 16:26:14 +040043 rootDir string
gio0eaf2712024-04-14 13:08:46 +040044 signer ssh.Signer
45 appDir string
46 runCommands []Command
47 self string
gioa60f0de2024-07-08 10:49:48 +040048 managerAddr string
gio183e8342024-08-20 06:01:24 +040049 logs *Log
gio45c31822024-10-24 10:58:02 +040050 currDir string
gio5155c1a2025-05-21 15:36:21 +040051 status *Status
gio0eaf2712024-04-14 13:08:46 +040052}
53
gioe65d9a92025-06-19 09:02:32 +040054func NewServer(agentMode bool, 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 +040055 return &Server{
56 l: &sync.Mutex{},
gioe65d9a92025-06-19 09:02:32 +040057 agentMode: agentMode,
gio0eaf2712024-04-14 13:08:46 +040058 port: port,
59 ready: false,
gio266c04f2024-07-03 14:18:45 +040060 appId: appId,
giob87415c2025-05-08 22:32:11 +040061 service: service,
gio5155c1a2025-05-21 15:36:21 +040062 id: id,
gio0eaf2712024-04-14 13:08:46 +040063 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040064 branch: branch,
giofc441e32024-11-11 16:26:14 +040065 rootDir: rootDir,
gio0eaf2712024-04-14 13:08:46 +040066 signer: signer,
67 appDir: appDir,
68 runCommands: runCommands,
69 self: self,
gioa60f0de2024-07-08 10:49:48 +040070 managerAddr: manager,
gio183e8342024-08-20 06:01:24 +040071 logs: &Log{},
gio45c31822024-10-24 10:58:02 +040072 currDir: "",
gio5155c1a2025-05-21 15:36:21 +040073 status: nil,
gio0eaf2712024-04-14 13:08:46 +040074 }
75}
76
77func (s *Server) Start() error {
78 http.HandleFunc("/update", s.handleUpdate)
79 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040080 http.HandleFunc("/logs", s.handleLogs)
gioec744fa2025-05-20 12:47:03 +040081 if s.managerAddr != "" && s.appId != "" {
82 go s.pingManager()
83 }
gio0eaf2712024-04-14 13:08:46 +040084 if err := s.run(); err != nil {
85 return err
86 }
gio0eaf2712024-04-14 13:08:46 +040087 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
88}
89
gio183e8342024-08-20 06:01:24 +040090func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
91 fmt.Fprint(w, s.logs.Contents())
92}
93
gio0eaf2712024-04-14 13:08:46 +040094func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
95 s.l.Lock()
96 defer s.l.Unlock()
97 if s.ready {
98 fmt.Fprintln(w, "ok")
99 } else {
100 http.Error(w, "not ready", http.StatusInternalServerError)
101 }
102}
103
104func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
105 fmt.Println("update")
106 s.l.Lock()
107 s.ready = false
108 s.l.Unlock()
gio4bfed002025-05-22 13:23:22 +0400109 fmt.Fprintf(s.logs, "!!! dodo: Reloading service\n")
gio0eaf2712024-04-14 13:08:46 +0400110 if err := s.run(); err != nil {
111 http.Error(w, err.Error(), http.StatusInternalServerError)
112 return
113 }
114 s.l.Lock()
115 s.ready = true
116 s.l.Unlock()
117}
118
gioe65d9a92025-06-19 09:02:32 +0400119type command struct {
120 cmd string
121 env []string
122}
123
gio0eaf2712024-04-14 13:08:46 +0400124func (s *Server) run() error {
gio8f8b0862025-07-01 13:31:42 +0400125 logM := io.MultiWriter(os.Stdout, s.logs)
gioe65d9a92025-06-19 09:02:32 +0400126 newDir := s.appDir
127 commands := []command{}
128 if !s.agentMode {
129 var err error
130 newDir, err = os.MkdirTemp(s.appDir, "code-*")
131 if err != nil {
132 return err
gio5155c1a2025-05-21 15:36:21 +0400133 }
gio0eaf2712024-04-14 13:08:46 +0400134 }
gioe65d9a92025-06-19 09:02:32 +0400135 if s.repoAddr != "" {
gioe8402352025-06-24 21:28:25 +0400136 if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
137 commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
138 if err != nil {
gio8f8b0862025-07-01 13:31:42 +0400139 fmt.Fprintf(logM, "!!! dodo: Failed to clone repository: %s\n", err)
gioe8402352025-06-24 21:28:25 +0400140 s.status = &Status{
141 Commit: nil,
142 }
143 return err
gioe65d9a92025-06-19 09:02:32 +0400144 }
gio8f8b0862025-07-01 13:31:42 +0400145 fmt.Fprintf(logM, "!!! dodo: Successfully cloned repository %s\n", commit.Hash)
gioe8402352025-06-24 21:28:25 +0400146 s.status = &Status{
147 Commit: commit,
148 Commands: []CommandStatus{},
149 }
150 } else {
151 s.status = &Status{
152 Commands: []CommandStatus{},
153 }
gioe65d9a92025-06-19 09:02:32 +0400154 }
155 } else {
156 s.status = &Status{
157 Commit: nil,
158 Commands: []CommandStatus{},
159 }
gio5155c1a2025-05-21 15:36:21 +0400160 }
gio24d6e9a2025-06-24 15:02:29 +0400161 if s.agentMode && s.repoAddr == "" {
gioe65d9a92025-06-19 09:02:32 +0400162 if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
163 commands = append(commands, command{cmd: "git config --global user.name dodo"})
164 s.status.Commands = append(s.status.Commands, CommandStatus{
165 Command: commands[len(commands)-1].cmd,
166 State: "waiting",
167 })
168 commands = append(commands, command{cmd: "git config --global user.email dodo@dodo.cloud"})
169 s.status.Commands = append(s.status.Commands, CommandStatus{
170 Command: commands[len(commands)-1].cmd,
171 State: "waiting",
172 })
173 commands = append(commands, command{cmd: "git init ."})
174 s.status.Commands = append(s.status.Commands, CommandStatus{
175 Command: commands[len(commands)-1].cmd,
176 State: "waiting",
177 })
178 commands = append(commands, command{cmd: "echo \"TODO: Describe project\" > README.md"})
179 s.status.Commands = append(s.status.Commands, CommandStatus{
180 Command: commands[len(commands)-1].cmd,
181 State: "waiting",
182 })
183 commands = append(commands, command{cmd: "git add README.md"})
184 s.status.Commands = append(s.status.Commands, CommandStatus{
185 Command: commands[len(commands)-1].cmd,
186 State: "waiting",
187 })
188 commands = append(commands, command{cmd: "git commit -m \"init\""})
189 s.status.Commands = append(s.status.Commands, CommandStatus{
190 Command: commands[len(commands)-1].cmd,
191 State: "waiting",
192 })
193 }
194 }
gio5155c1a2025-05-21 15:36:21 +0400195 for _, c := range s.runCommands {
gio0eaf2712024-04-14 13:08:46 +0400196 args := []string{c.Bin}
197 args = append(args, c.Args...)
gio5155c1a2025-05-21 15:36:21 +0400198 cmd := strings.Join(args, " ")
gioe65d9a92025-06-19 09:02:32 +0400199 commands = append(commands, command{cmd, c.Env})
gio5155c1a2025-05-21 15:36:21 +0400200 s.status.Commands = append(s.status.Commands, CommandStatus{
201 Command: cmd,
202 State: "waiting",
203 })
204 }
gio5155c1a2025-05-21 15:36:21 +0400205 for i, c := range commands {
206 if i > 0 {
207 s.status.Commands[i-1].State = "success"
208 }
gio0eaf2712024-04-14 13:08:46 +0400209 cmd := &exec.Cmd{
giofc441e32024-11-11 16:26:14 +0400210 Dir: filepath.Join(newDir, s.rootDir),
giobcd25e92025-05-03 19:14:10 +0400211 Path: "/bin/sh",
gioe65d9a92025-06-19 09:02:32 +0400212 Args: []string{"/bin/sh", "-c", c.cmd},
213 Env: append(os.Environ(), c.env...),
gio183e8342024-08-20 06:01:24 +0400214 Stdout: logM,
215 Stderr: logM,
gio0eaf2712024-04-14 13:08:46 +0400216 }
gioff0ee0f2024-10-15 23:11:54 +0400217 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gio8f8b0862025-07-01 13:31:42 +0400218 fmt.Fprintf(logM, "!!! dodo: Running: %s\n", c)
gio5155c1a2025-05-21 15:36:21 +0400219 s.status.Commands[i].State = "running"
gioe65d9a92025-06-19 09:02:32 +0400220 if i < len(commands)-1 {
gio0eaf2712024-04-14 13:08:46 +0400221 if err := cmd.Run(); err != nil {
222 return err
223 }
224 } else {
gio45c31822024-10-24 10:58:02 +0400225 if s.cmd != nil {
226 // TODO(gio): make this point configurable
227 if err := s.kill(); err != nil {
228 return err
229 }
gioe65d9a92025-06-19 09:02:32 +0400230 if s.currDir != "" && !s.agentMode {
gio5155c1a2025-05-21 15:36:21 +0400231 if err := os.RemoveAll(s.currDir); err != nil {
232 return err
233 }
gio45c31822024-10-24 10:58:02 +0400234 }
235 }
gio0eaf2712024-04-14 13:08:46 +0400236 if err := cmd.Start(); err != nil {
237 return err
238 }
239 s.cmd = cmd
240 }
241 }
gio45c31822024-10-24 10:58:02 +0400242 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400243 return nil
244}
245
246type pingReq struct {
gio5155c1a2025-05-21 15:36:21 +0400247 Id string `json:"id"`
248 Service string `json:"service"`
249 Address string `json:"address"`
250 Status *Status `json:"status,omitempty"`
251 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400252}
253
254func (s *Server) pingManager() {
255 defer func() {
256 go func() {
257 time.Sleep(5 * time.Second)
258 s.pingManager()
259 }()
260 }()
gioa60f0de2024-07-08 10:49:48 +0400261 buf, err := json.Marshal(pingReq{
gio5155c1a2025-05-21 15:36:21 +0400262 Id: s.id,
giob87415c2025-05-08 22:32:11 +0400263 Service: s.service,
264 Address: fmt.Sprintf("http://%s:%d", s.self, s.port),
gio5155c1a2025-05-21 15:36:21 +0400265 Status: s.status,
gio183e8342024-08-20 06:01:24 +0400266 Logs: s.logs.Contents(),
gioa60f0de2024-07-08 10:49:48 +0400267 })
gio0eaf2712024-04-14 13:08:46 +0400268 if err != nil {
269 return
270 }
giob87415c2025-05-08 22:32:11 +0400271 registerWorkerAddr := fmt.Sprintf("%s/api/project/%s/workers", s.managerAddr, s.appId)
272 resp, err := http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
273 if err != nil {
274 fmt.Println(err)
275 } else {
276 // check resp code
277 io.Copy(os.Stdout, resp.Body)
278 }
gio0eaf2712024-04-14 13:08:46 +0400279}
gio45c31822024-10-24 10:58:02 +0400280
281func (s *Server) kill() error {
282 if s.cmd == nil {
283 return nil
284 }
285
286 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
287 if err != nil {
288 return err
289 }
290 // NOTE(gio): No need to check err as we just killed the process
291 s.cmd.Wait()
292 s.cmd = nil
293 return nil
294}