blob: f903d9b5a0c265e50601c7dcb5a0ecd608d74723 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "strings"
9 "time"
10
11 "github.com/giolekva/pcloud/core/installer"
12 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040013
14 "github.com/gorilla/mux"
gio0eaf2712024-04-14 13:08:46 +040015)
16
17type DodoAppServer struct {
gio33059762024-07-05 13:19:07 +040018 port int
19 self string
20 sshKey string
21 gitRepoPublicKey string
22 client soft.Client
23 namespace string
24 env installer.EnvConfig
25 nsc installer.NamespaceCreator
26 jc installer.JobCreator
27 workers map[string]map[string]struct{}
28 appNs map[string]string
gio0eaf2712024-04-14 13:08:46 +040029}
30
gio33059762024-07-05 13:19:07 +040031// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040032func NewDodoAppServer(
33 port int,
gio33059762024-07-05 13:19:07 +040034 self string,
gio0eaf2712024-04-14 13:08:46 +040035 sshKey string,
gio33059762024-07-05 13:19:07 +040036 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +040037 client soft.Client,
38 namespace string,
gio33059762024-07-05 13:19:07 +040039 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040040 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +040041 env installer.EnvConfig,
42) *DodoAppServer {
43 return &DodoAppServer{
44 port,
gio33059762024-07-05 13:19:07 +040045 self,
gio0eaf2712024-04-14 13:08:46 +040046 sshKey,
gio33059762024-07-05 13:19:07 +040047 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +040048 client,
49 namespace,
50 env,
gio33059762024-07-05 13:19:07 +040051 nsc,
giof8843412024-05-22 16:38:05 +040052 jc,
gio266c04f2024-07-03 14:18:45 +040053 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +040054 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +040055 }
56}
57
58func (s *DodoAppServer) Start() error {
gio33059762024-07-05 13:19:07 +040059 r := mux.NewRouter()
60 r.HandleFunc("/update", s.handleUpdate)
61 r.HandleFunc("/register-worker", s.handleRegisterWorker).Methods(http.MethodPost)
62 r.HandleFunc("/api/apps", s.handleCreateApp).Methods(http.MethodPost)
63 r.HandleFunc("/api/add-admin-key", s.handleAddAdminKey).Methods(http.MethodPost)
64 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
gio0eaf2712024-04-14 13:08:46 +040065}
66
67type updateReq struct {
gio266c04f2024-07-03 14:18:45 +040068 Ref string `json:"ref"`
69 Repository struct {
70 Name string `json:"name"`
71 } `json:"repository"`
gio0eaf2712024-04-14 13:08:46 +040072}
73
74func (s *DodoAppServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
75 fmt.Println("update")
76 var req updateReq
77 var contents strings.Builder
78 io.Copy(&contents, r.Body)
79 c := contents.String()
80 fmt.Println(c)
81 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
82 fmt.Println(err)
83 return
84 }
gio33059762024-07-05 13:19:07 +040085 if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "config") {
gio0eaf2712024-04-14 13:08:46 +040086 return
87 }
88 go func() {
89 time.Sleep(20 * time.Second)
gio33059762024-07-05 13:19:07 +040090 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
gio0eaf2712024-04-14 13:08:46 +040091 fmt.Println(err)
92 }
93 }()
gio266c04f2024-07-03 14:18:45 +040094 for addr, _ := range s.workers[req.Repository.Name] {
gio0eaf2712024-04-14 13:08:46 +040095 go func() {
96 // TODO(gio): make port configurable
97 http.Get(fmt.Sprintf("http://%s:3000/update", addr))
98 }()
99 }
100}
101
102type registerWorkerReq struct {
gio266c04f2024-07-03 14:18:45 +0400103 AppId string `json:"appId"`
gio0eaf2712024-04-14 13:08:46 +0400104 Address string `json:"address"`
105}
106
107func (s *DodoAppServer) handleRegisterWorker(w http.ResponseWriter, r *http.Request) {
108 var req registerWorkerReq
109 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
110 http.Error(w, err.Error(), http.StatusInternalServerError)
111 return
112 }
gio266c04f2024-07-03 14:18:45 +0400113 if _, ok := s.workers[req.AppId]; !ok {
114 s.workers[req.AppId] = map[string]struct{}{}
115 }
116 s.workers[req.AppId][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400117}
118
gio33059762024-07-05 13:19:07 +0400119type createAppReq struct {
120 AdminPublicKey string `json:"adminPublicKey"`
121}
122
123type createAppResp struct {
124 AppName string `json:"appName"`
125}
126
127func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
128 var req createAppReq
129 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
130 http.Error(w, err.Error(), http.StatusBadRequest)
131 return
132 }
133 g := installer.NewFixedLengthRandomNameGenerator(3)
134 appName, err := g.Generate()
135 if err != nil {
136 http.Error(w, err.Error(), http.StatusInternalServerError)
137 return
138 }
139 if err := s.CreateApp(appName, req.AdminPublicKey); err != nil {
140 http.Error(w, err.Error(), http.StatusInternalServerError)
141 return
142 }
143 resp := createAppResp{appName}
144 if err := json.NewEncoder(w).Encode(resp); err != nil {
145 http.Error(w, err.Error(), http.StatusInternalServerError)
146 return
147 }
148}
149
150func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) error {
151 fmt.Printf("Creating app: %s\n", appName)
152 if ok, err := s.client.RepoExists(appName); err != nil {
153 return err
154 } else if ok {
155 return nil
156 }
157 if err := s.client.AddRepository(appName); err != nil {
158 return err
159 }
160 appRepo, err := s.client.GetRepo(appName)
161 if err != nil {
162 return err
163 }
164 if err := InitRepo(appRepo); err != nil {
165 return err
166 }
167 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
168 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
169 if err != nil {
170 return err
171 }
172 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
173 suffix, err := suffixGen.Generate()
174 if err != nil {
175 return err
176 }
177 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
178 s.appNs[appName] = namespace
179 if err := s.updateDodoApp(appName, namespace); err != nil {
180 return err
181 }
182 if ok, err := s.client.RepoExists("config"); err != nil {
183 return err
184 } else if !ok {
185 if err := s.client.AddRepository("config"); err != nil {
186 return err
187 }
188 }
189 repo, err := s.client.GetRepo("config")
190 if err != nil {
191 return err
192 }
193 hf := installer.NewGitHelmFetcher()
194 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
195 if err != nil {
196 return err
197 }
198 if _, err := m.Install(app, appName, "/"+appName, namespace, map[string]any{
199 "repoAddr": s.client.GetRepoAddress(appName),
200 "repoHost": strings.Split(s.client.Address(), ":")[0],
201 "gitRepoPublicKey": s.gitRepoPublicKey,
202 }, installer.WithConfig(&s.env)); err != nil {
203 return err
204 }
205 cfg, err := m.FindInstance(appName)
206 if err != nil {
207 return err
208 }
209 fluxKeys, ok := cfg.Input["fluxKeys"]
210 if !ok {
211 return fmt.Errorf("Fluxcd keys not found")
212 }
213 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
214 if !ok {
215 return fmt.Errorf("Fluxcd keys not found")
216 }
217 if ok, err := s.client.UserExists("fluxcd"); err != nil {
218 return err
219 } else if ok {
220 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
221 return err
222 }
223 } else {
224 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
225 return err
226 }
227 }
228 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
229 return err
230 }
231 if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
232 return err
233 }
234 if user, err := s.client.FindUser(adminPublicKey); err != nil {
235 return err
236 } else if user != "" {
237 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
238 return err
239 }
240 } else {
241 if err := s.client.AddUser(appName, adminPublicKey); err != nil {
242 return err
243 }
244 if err := s.client.AddReadWriteCollaborator(appName, appName); err != nil {
245 return err
246 }
247 }
248 return nil
249}
250
gio70be3e52024-06-26 18:27:19 +0400251type addAdminKeyReq struct {
252 Public string `json:"public"`
253}
254
255func (s *DodoAppServer) handleAddAdminKey(w http.ResponseWriter, r *http.Request) {
256 var req addAdminKeyReq
257 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
258 http.Error(w, err.Error(), http.StatusBadRequest)
259 return
260 }
261 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
262 http.Error(w, err.Error(), http.StatusInternalServerError)
263 return
264 }
265}
266
gio33059762024-07-05 13:19:07 +0400267func (s *DodoAppServer) updateDodoApp(name, namespace string) error {
268 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400269 if err != nil {
270 return err
271 }
giof8843412024-05-22 16:38:05 +0400272 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400273 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400274 if err != nil {
275 return err
276 }
277 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400278 if err != nil {
279 return err
280 }
281 app, err := installer.NewDodoApp(appCfg)
282 if err != nil {
283 return err
284 }
giof8843412024-05-22 16:38:05 +0400285 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400286 if _, err := m.Install(
287 app,
288 "app",
289 "/.dodo/app",
290 namespace,
291 map[string]any{
gio33059762024-07-05 13:19:07 +0400292 "repoAddr": repo.FullAddress(),
293 "registerWorkerAddr": fmt.Sprintf("http://%s/register-worker", s.self),
294 "appId": name,
295 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400296 },
gio33059762024-07-05 13:19:07 +0400297 installer.WithConfig(&s.env),
giof71a0832024-06-27 14:45:45 +0400298 installer.WithLocalChartGenerator(lg),
299 installer.WithBranch("dodo"),
300 installer.WithForce(),
301 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400302 return err
303 }
304 return nil
305}
gio33059762024-07-05 13:19:07 +0400306
307const goMod = `module dodo.app
308
309go 1.18
310`
311
312const mainGo = `package main
313
314import (
315 "flag"
316 "fmt"
317 "log"
318 "net/http"
319)
320
321var port = flag.Int("port", 8080, "Port to listen on")
322
323func handler(w http.ResponseWriter, r *http.Request) {
324 fmt.Fprintln(w, "Hello from Dodo App!")
325}
326
327func main() {
328 flag.Parse()
329 http.HandleFunc("/", handler)
330 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
331}
332`
333
334const appCue = `app: {
335 type: "golang:1.22.0"
336 run: "main.go"
337 ingress: {
338 network: "Private" // or Public
339 subdomain: "testapp"
340 auth: enabled: false
341 }
342}
343`
344
345func InitRepo(repo soft.RepoIO) error {
346 return repo.Do(func(fs soft.RepoFS) (string, error) {
347 {
348 w, err := fs.Writer("go.mod")
349 if err != nil {
350 return "", err
351 }
352 defer w.Close()
353 fmt.Fprint(w, goMod)
354 }
355 {
356 w, err := fs.Writer("main.go")
357 if err != nil {
358 return "", err
359 }
360 defer w.Close()
361 fmt.Fprintf(w, "%s", mainGo)
362 }
363 {
364 w, err := fs.Writer("app.cue")
365 if err != nil {
366 return "", err
367 }
368 defer w.Close()
369 fmt.Fprint(w, appCue)
370 }
371 return "go web app template", nil
372 })
373}