blob: 1661731ed1586f72c558211b5b85d22585cec0ad [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 {
28 RepoOK bool `json:"repoOK"`
29 Commit string `json:"commit"`
30 Commands []CommandStatus `json:"commands"`
31}
32
gio0eaf2712024-04-14 13:08:46 +040033type Server struct {
34 l sync.Locker
35 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
gio5155c1a2025-05-21 15:36:21 +040054func 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 +040055 return &Server{
56 l: &sync.Mutex{},
57 port: port,
58 ready: false,
gio266c04f2024-07-03 14:18:45 +040059 appId: appId,
giob87415c2025-05-08 22:32:11 +040060 service: service,
gio5155c1a2025-05-21 15:36:21 +040061 id: id,
gio0eaf2712024-04-14 13:08:46 +040062 repoAddr: repoAddr,
gio2b1157a2024-10-24 08:45:07 +040063 branch: branch,
giofc441e32024-11-11 16:26:14 +040064 rootDir: rootDir,
gio0eaf2712024-04-14 13:08:46 +040065 signer: signer,
66 appDir: appDir,
67 runCommands: runCommands,
68 self: self,
gioa60f0de2024-07-08 10:49:48 +040069 managerAddr: manager,
gio183e8342024-08-20 06:01:24 +040070 logs: &Log{},
gio45c31822024-10-24 10:58:02 +040071 currDir: "",
gio5155c1a2025-05-21 15:36:21 +040072 status: nil,
gio0eaf2712024-04-14 13:08:46 +040073 }
74}
75
76func (s *Server) Start() error {
77 http.HandleFunc("/update", s.handleUpdate)
78 http.HandleFunc("/ready", s.handleReady)
gio183e8342024-08-20 06:01:24 +040079 http.HandleFunc("/logs", s.handleLogs)
gioec744fa2025-05-20 12:47:03 +040080 if s.managerAddr != "" && s.appId != "" {
81 go s.pingManager()
82 }
gio0eaf2712024-04-14 13:08:46 +040083 if err := s.run(); err != nil {
84 return err
85 }
gio0eaf2712024-04-14 13:08:46 +040086 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
87}
88
gio183e8342024-08-20 06:01:24 +040089func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
90 fmt.Fprint(w, s.logs.Contents())
91}
92
gio0eaf2712024-04-14 13:08:46 +040093func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
94 s.l.Lock()
95 defer s.l.Unlock()
96 if s.ready {
97 fmt.Fprintln(w, "ok")
98 } else {
99 http.Error(w, "not ready", http.StatusInternalServerError)
100 }
101}
102
103func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
104 fmt.Println("update")
105 s.l.Lock()
106 s.ready = false
107 s.l.Unlock()
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 {
124 s.status = &Status{
125 RepoOK: false,
126 }
gio0eaf2712024-04-14 13:08:46 +0400127 return err
128 }
gio5155c1a2025-05-21 15:36:21 +0400129 s.status = &Status{
130 RepoOK: true,
131 Commit: commit,
132 Commands: []CommandStatus{},
133 }
134 commands := []string{}
135 for _, c := range s.runCommands {
gio0eaf2712024-04-14 13:08:46 +0400136 args := []string{c.Bin}
137 args = append(args, c.Args...)
gio5155c1a2025-05-21 15:36:21 +0400138 cmd := strings.Join(args, " ")
139 commands = append(commands, cmd)
140 s.status.Commands = append(s.status.Commands, CommandStatus{
141 Command: cmd,
142 State: "waiting",
143 })
144 }
145 logM := io.MultiWriter(os.Stdout, s.logs)
146 for i, c := range commands {
147 if i > 0 {
148 s.status.Commands[i-1].State = "success"
149 }
gio0eaf2712024-04-14 13:08:46 +0400150 cmd := &exec.Cmd{
giofc441e32024-11-11 16:26:14 +0400151 Dir: filepath.Join(newDir, s.rootDir),
giobcd25e92025-05-03 19:14:10 +0400152 Path: "/bin/sh",
gio5155c1a2025-05-21 15:36:21 +0400153 Args: []string{"/bin/sh", "-c", c},
154 Env: append(os.Environ(), s.runCommands[i].Env...),
gio183e8342024-08-20 06:01:24 +0400155 Stdout: logM,
156 Stderr: logM,
gio0eaf2712024-04-14 13:08:46 +0400157 }
gioff0ee0f2024-10-15 23:11:54 +0400158 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
gio5155c1a2025-05-21 15:36:21 +0400159 fmt.Printf("Running: %s\n", c)
160 s.status.Commands[i].State = "running"
gio0eaf2712024-04-14 13:08:46 +0400161 if i < len(s.runCommands)-1 {
162 if err := cmd.Run(); err != nil {
163 return err
164 }
165 } else {
gio45c31822024-10-24 10:58:02 +0400166 if s.cmd != nil {
167 // TODO(gio): make this point configurable
168 if err := s.kill(); err != nil {
169 return err
170 }
gio5155c1a2025-05-21 15:36:21 +0400171 if s.currDir != "" {
172 if err := os.RemoveAll(s.currDir); err != nil {
173 return err
174 }
gio45c31822024-10-24 10:58:02 +0400175 }
176 }
gio0eaf2712024-04-14 13:08:46 +0400177 if err := cmd.Start(); err != nil {
178 return err
179 }
180 s.cmd = cmd
181 }
182 }
gio45c31822024-10-24 10:58:02 +0400183 s.currDir = newDir
gio0eaf2712024-04-14 13:08:46 +0400184 return nil
185}
186
187type pingReq struct {
gio5155c1a2025-05-21 15:36:21 +0400188 Id string `json:"id"`
189 Service string `json:"service"`
190 Address string `json:"address"`
191 Status *Status `json:"status,omitempty"`
192 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400193}
194
195func (s *Server) pingManager() {
196 defer func() {
197 go func() {
198 time.Sleep(5 * time.Second)
199 s.pingManager()
200 }()
201 }()
gioa60f0de2024-07-08 10:49:48 +0400202 buf, err := json.Marshal(pingReq{
gio5155c1a2025-05-21 15:36:21 +0400203 Id: s.id,
giob87415c2025-05-08 22:32:11 +0400204 Service: s.service,
205 Address: fmt.Sprintf("http://%s:%d", s.self, s.port),
gio5155c1a2025-05-21 15:36:21 +0400206 Status: s.status,
gio183e8342024-08-20 06:01:24 +0400207 Logs: s.logs.Contents(),
gioa60f0de2024-07-08 10:49:48 +0400208 })
gio0eaf2712024-04-14 13:08:46 +0400209 if err != nil {
210 return
211 }
giob87415c2025-05-08 22:32:11 +0400212 registerWorkerAddr := fmt.Sprintf("%s/api/project/%s/workers", s.managerAddr, s.appId)
213 resp, err := http.Post(registerWorkerAddr, "application/json", bytes.NewReader(buf))
214 if err != nil {
215 fmt.Println(err)
216 } else {
217 // check resp code
218 io.Copy(os.Stdout, resp.Body)
219 }
gio0eaf2712024-04-14 13:08:46 +0400220}
gio45c31822024-10-24 10:58:02 +0400221
222func (s *Server) kill() error {
223 if s.cmd == nil {
224 return nil
225 }
226
227 err := syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL)
228 if err != nil {
229 return err
230 }
231 // NOTE(gio): No need to check err as we just killed the process
232 s.cmd.Wait()
233 s.cmd = nil
234 return nil
235}