blob: 1315266504a666703f091ffd885a506e325a8d0a [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
gio81246f02024-07-10 12:02:15 +04004 "context"
gio23bdc1b2024-07-11 16:07:47 +04005 "embed"
gio0eaf2712024-04-14 13:08:46 +04006 "encoding/json"
gio9d66f322024-07-06 13:45:10 +04007 "errors"
gio0eaf2712024-04-14 13:08:46 +04008 "fmt"
gio81246f02024-07-10 12:02:15 +04009 "golang.org/x/crypto/bcrypt"
gio23bdc1b2024-07-11 16:07:47 +040010 "html/template"
gio0eaf2712024-04-14 13:08:46 +040011 "io"
gio9d66f322024-07-06 13:45:10 +040012 "io/fs"
gio0eaf2712024-04-14 13:08:46 +040013 "net/http"
gio23bdc1b2024-07-11 16:07:47 +040014 "slices"
gio0eaf2712024-04-14 13:08:46 +040015 "strings"
gio9d66f322024-07-06 13:45:10 +040016 "sync"
gio0eaf2712024-04-14 13:08:46 +040017
18 "github.com/giolekva/pcloud/core/installer"
19 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040020
21 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040022 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040023)
24
gio23bdc1b2024-07-11 16:07:47 +040025//go:embed dodo-app-tmpl/*
26var dodoAppTmplFS embed.FS
27
gio9d66f322024-07-06 13:45:10 +040028const (
gioa60f0de2024-07-08 10:49:48 +040029 ConfigRepoName = "config"
gio9d66f322024-07-06 13:45:10 +040030 namespacesFile = "/namespaces.json"
gio81246f02024-07-10 12:02:15 +040031 loginPath = "/login"
32 logoutPath = "/logout"
33 sessionCookie = "dodo-app-session"
34 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040035)
36
gio23bdc1b2024-07-11 16:07:47 +040037type dodoAppTmplts struct {
38 index *template.Template
39}
40
41func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
42 index, err := template.New("index.html").ParseFS(fs, "dodo-app-tmpl/index.html")
43 if err != nil {
44 return dodoAppTmplts{}, err
45 }
46 return dodoAppTmplts{index}, nil
47}
48
gio0eaf2712024-04-14 13:08:46 +040049type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040050 l sync.Locker
51 st Store
gio11617ac2024-07-15 16:09:04 +040052 nf NetworkFilter
53 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040054 port int
55 apiPort int
56 self string
gio11617ac2024-07-15 16:09:04 +040057 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040058 sshKey string
59 gitRepoPublicKey string
60 client soft.Client
61 namespace string
62 envAppManagerAddr string
63 env installer.EnvConfig
64 nsc installer.NamespaceCreator
65 jc installer.JobCreator
66 workers map[string]map[string]struct{}
67 appNs map[string]string
68 sc *securecookie.SecureCookie
gio23bdc1b2024-07-11 16:07:47 +040069 tmplts dodoAppTmplts
gio0eaf2712024-04-14 13:08:46 +040070}
71
gio33059762024-07-05 13:19:07 +040072// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040073func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +040074 st Store,
gio11617ac2024-07-15 16:09:04 +040075 nf NetworkFilter,
76 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +040077 port int,
gioa60f0de2024-07-08 10:49:48 +040078 apiPort int,
gio33059762024-07-05 13:19:07 +040079 self string,
gio11617ac2024-07-15 16:09:04 +040080 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +040081 sshKey string,
gio33059762024-07-05 13:19:07 +040082 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +040083 client soft.Client,
84 namespace string,
giocb34ad22024-07-11 08:01:13 +040085 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +040086 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040087 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +040088 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +040089) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +040090 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
91 if err != nil {
92 return nil, err
93 }
gio81246f02024-07-10 12:02:15 +040094 sc := securecookie.New(
95 securecookie.GenerateRandomKey(64),
96 securecookie.GenerateRandomKey(32),
97 )
gio9d66f322024-07-06 13:45:10 +040098 s := &DodoAppServer{
99 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400100 st,
gio11617ac2024-07-15 16:09:04 +0400101 nf,
102 ug,
gio0eaf2712024-04-14 13:08:46 +0400103 port,
gioa60f0de2024-07-08 10:49:48 +0400104 apiPort,
gio33059762024-07-05 13:19:07 +0400105 self,
gio11617ac2024-07-15 16:09:04 +0400106 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400107 sshKey,
gio33059762024-07-05 13:19:07 +0400108 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400109 client,
110 namespace,
giocb34ad22024-07-11 08:01:13 +0400111 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400112 env,
gio33059762024-07-05 13:19:07 +0400113 nsc,
giof8843412024-05-22 16:38:05 +0400114 jc,
gio266c04f2024-07-03 14:18:45 +0400115 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +0400116 map[string]string{},
gio81246f02024-07-10 12:02:15 +0400117 sc,
gio23bdc1b2024-07-11 16:07:47 +0400118 tmplts,
gio0eaf2712024-04-14 13:08:46 +0400119 }
gioa60f0de2024-07-08 10:49:48 +0400120 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400121 if err != nil {
122 return nil, err
123 }
124 r, err := config.Reader(namespacesFile)
125 if err == nil {
126 defer r.Close()
127 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
128 return nil, err
129 }
130 } else if !errors.Is(err, fs.ErrNotExist) {
131 return nil, err
132 }
133 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400134}
135
136func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400137 e := make(chan error)
138 go func() {
139 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400140 r.Use(s.mwAuth)
141 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
142 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
143 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
144 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
145 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400146 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400147 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
148 }()
149 go func() {
150 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400151 r.HandleFunc("/update", s.handleApiUpdate)
152 r.HandleFunc("/api/apps/{app-name}/workers", s.handleApiRegisterWorker).Methods(http.MethodPost)
153 r.HandleFunc("/api/apps", s.handleApiCreateApp).Methods(http.MethodPost)
154 r.HandleFunc("/api/add-admin-key", s.handleApiAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400155 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
156 }()
157 return <-e
158}
159
gio11617ac2024-07-15 16:09:04 +0400160type UserGetter interface {
161 Get(r *http.Request) string
162}
163
164type externalUserGetter struct {
165 sc *securecookie.SecureCookie
166}
167
168func NewExternalUserGetter() UserGetter {
169 return &externalUserGetter{}
170}
171
172func (ug *externalUserGetter) Get(r *http.Request) string {
173 cookie, err := r.Cookie(sessionCookie)
174 if err != nil {
175 return ""
176 }
177 var user string
178 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
179 return ""
180 }
181 return user
182}
183
184type internalUserGetter struct{}
185
186func NewInternalUserGetter() UserGetter {
187 return internalUserGetter{}
188}
189
190func (ug internalUserGetter) Get(r *http.Request) string {
191 return r.Header.Get("X-User")
192}
193
gio81246f02024-07-10 12:02:15 +0400194func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
195 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
196 if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) {
197 next.ServeHTTP(w, r)
198 return
199 }
gio11617ac2024-07-15 16:09:04 +0400200 user := s.ug.Get(r)
201 if user == "" {
gio81246f02024-07-10 12:02:15 +0400202 vars := mux.Vars(r)
203 appName, ok := vars["app-name"]
204 if !ok || appName == "" {
205 http.Error(w, "missing app-name", http.StatusBadRequest)
206 return
207 }
208 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
209 return
210 }
gio81246f02024-07-10 12:02:15 +0400211 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
212 })
213}
214
215func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
216 http.SetCookie(w, &http.Cookie{
217 Name: sessionCookie,
218 Value: "",
219 Path: "/",
220 HttpOnly: true,
221 Secure: true,
222 })
223 http.Redirect(w, r, "/", http.StatusSeeOther)
224}
225
226func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
227 vars := mux.Vars(r)
228 appName, ok := vars["app-name"]
229 if !ok || appName == "" {
230 http.Error(w, "missing app-name", http.StatusBadRequest)
231 return
232 }
233 fmt.Fprint(w, `
234<!DOCTYPE html>
235<html lang='en'>
236 <head>
237 <title>dodo: app - login</title>
238 <meta charset='utf-8'>
239 </head>
240 <body>
241 <form action="" method="POST">
242 <input type="password" placeholder="Password" name="password" required />
243 <button type="submit">Login</button>
244 </form>
245 </body>
246</html>
247`)
248}
249
250func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
251 vars := mux.Vars(r)
252 appName, ok := vars["app-name"]
253 if !ok || appName == "" {
254 http.Error(w, "missing app-name", http.StatusBadRequest)
255 return
256 }
257 password := r.FormValue("password")
258 if password == "" {
259 http.Error(w, "missing password", http.StatusBadRequest)
260 return
261 }
262 user, err := s.st.GetAppOwner(appName)
263 if err != nil {
264 http.Error(w, err.Error(), http.StatusInternalServerError)
265 return
266 }
267 hashed, err := s.st.GetUserPassword(user)
268 if err != nil {
269 http.Error(w, err.Error(), http.StatusInternalServerError)
270 return
271 }
272 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
273 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
274 return
275 }
276 if encoded, err := s.sc.Encode(sessionCookie, user); err == nil {
277 cookie := &http.Cookie{
278 Name: sessionCookie,
279 Value: encoded,
280 Path: "/",
281 Secure: true,
282 HttpOnly: true,
283 }
284 http.SetCookie(w, cookie)
285 }
286 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
287}
288
gio23bdc1b2024-07-11 16:07:47 +0400289type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400290 Apps []string
291 Networks []installer.Network
gio23bdc1b2024-07-11 16:07:47 +0400292}
293
gioa60f0de2024-07-08 10:49:48 +0400294func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400295 user := r.Context().Value(userCtx)
296 if user == nil {
297 http.Error(w, "unauthorized", http.StatusUnauthorized)
298 return
299 }
300 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400301 if err != nil {
302 http.Error(w, err.Error(), http.StatusInternalServerError)
303 return
304 }
gio11617ac2024-07-15 16:09:04 +0400305 networks, err := s.getNetworks(user.(string))
306 if err != nil {
307 http.Error(w, err.Error(), http.StatusInternalServerError)
308 return
309 }
310 data := statusData{apps, networks}
gio23bdc1b2024-07-11 16:07:47 +0400311 if err := s.tmplts.index.Execute(w, data); err != nil {
312 http.Error(w, err.Error(), http.StatusInternalServerError)
313 return
gioa60f0de2024-07-08 10:49:48 +0400314 }
315}
316
317func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
318 vars := mux.Vars(r)
319 appName, ok := vars["app-name"]
320 if !ok || appName == "" {
321 http.Error(w, "missing app-name", http.StatusBadRequest)
322 return
323 }
gio11617ac2024-07-15 16:09:04 +0400324 fmt.Fprintf(w, "git clone %s/%s\n\n\n", s.repoPublicAddr, appName)
gioa60f0de2024-07-08 10:49:48 +0400325 commits, err := s.st.GetCommitHistory(appName)
326 if err != nil {
327 http.Error(w, err.Error(), http.StatusInternalServerError)
328 return
329 }
330 for _, c := range commits {
331 fmt.Fprintf(w, "%s %s\n", c.Hash, c.Message)
332 }
gio0eaf2712024-04-14 13:08:46 +0400333}
334
gio81246f02024-07-10 12:02:15 +0400335type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400336 Ref string `json:"ref"`
337 Repository struct {
338 Name string `json:"name"`
339 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400340 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400341}
342
gio81246f02024-07-10 12:02:15 +0400343func (s *DodoAppServer) handleApiUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400344 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400345 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400346 var contents strings.Builder
347 io.Copy(&contents, r.Body)
348 c := contents.String()
349 fmt.Println(c)
350 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400351 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400352 return
353 }
gioa60f0de2024-07-08 10:49:48 +0400354 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400355 return
356 }
gioa60f0de2024-07-08 10:49:48 +0400357 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400358 go func() {
gio11617ac2024-07-15 16:09:04 +0400359 owner, err := s.st.GetAppOwner(req.Repository.Name)
360 if err != nil {
361 return
362 }
363 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400364 if err != nil {
365 return
366 }
367 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400368 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
369 fmt.Printf("Error: %s\n", err.Error())
370 return
371 }
372 }
373 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
374 fmt.Printf("Error: %s\n", err.Error())
375 }
376 for addr, _ := range s.workers[req.Repository.Name] {
377 go func() {
378 // TODO(gio): make port configurable
379 http.Get(fmt.Sprintf("http://%s/update", addr))
380 }()
gio0eaf2712024-04-14 13:08:46 +0400381 }
382 }()
gio0eaf2712024-04-14 13:08:46 +0400383}
384
gio81246f02024-07-10 12:02:15 +0400385type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400386 Address string `json:"address"`
387}
388
gio81246f02024-07-10 12:02:15 +0400389func (s *DodoAppServer) handleApiRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400390 vars := mux.Vars(r)
391 appName, ok := vars["app-name"]
392 if !ok || appName == "" {
393 http.Error(w, "missing app-name", http.StatusBadRequest)
394 return
395 }
gio81246f02024-07-10 12:02:15 +0400396 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400397 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
398 http.Error(w, err.Error(), http.StatusInternalServerError)
399 return
400 }
gioa60f0de2024-07-08 10:49:48 +0400401 if _, ok := s.workers[appName]; !ok {
402 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400403 }
gioa60f0de2024-07-08 10:49:48 +0400404 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400405}
406
gio11617ac2024-07-15 16:09:04 +0400407func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
408 u := r.Context().Value(userCtx)
409 if u == nil {
410 http.Error(w, "unauthorized", http.StatusUnauthorized)
411 return
412 }
413 user, ok := u.(string)
414 if !ok {
415 http.Error(w, "could not get user", http.StatusInternalServerError)
416 return
417 }
418 network := r.FormValue("network")
419 if network == "" {
420 http.Error(w, "missing network", http.StatusBadRequest)
421 return
422 }
423 adminPublicKey := r.FormValue("admin-public-key")
424 if network == "" {
425 http.Error(w, "missing admin public key", http.StatusBadRequest)
426 return
427 }
428 g := installer.NewFixedLengthRandomNameGenerator(3)
429 appName, err := g.Generate()
430 if err != nil {
431 http.Error(w, err.Error(), http.StatusInternalServerError)
432 return
433 }
434 if ok, err := s.client.UserExists(user); err != nil {
435 http.Error(w, err.Error(), http.StatusInternalServerError)
436 return
437 } else if !ok {
438 if err := s.client.AddUser(user, adminPublicKey); err != nil {
439 http.Error(w, err.Error(), http.StatusInternalServerError)
440 return
441 }
442 }
443 if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
444 http.Error(w, err.Error(), http.StatusInternalServerError)
445 return
446 }
447 if err := s.st.CreateApp(appName, user); err != nil {
448 http.Error(w, err.Error(), http.StatusInternalServerError)
449 return
450 }
451 if err := s.CreateApp(user, appName, adminPublicKey, network); err != nil {
452 http.Error(w, err.Error(), http.StatusInternalServerError)
453 return
454 }
455 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
456}
457
gio81246f02024-07-10 12:02:15 +0400458type apiCreateAppReq struct {
gio33059762024-07-05 13:19:07 +0400459 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400460 Network string `json:"network"`
gio33059762024-07-05 13:19:07 +0400461}
462
gio81246f02024-07-10 12:02:15 +0400463type apiCreateAppResp struct {
464 AppName string `json:"appName"`
465 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400466}
467
gio81246f02024-07-10 12:02:15 +0400468func (s *DodoAppServer) handleApiCreateApp(w http.ResponseWriter, r *http.Request) {
469 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400470 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
471 http.Error(w, err.Error(), http.StatusBadRequest)
472 return
473 }
474 g := installer.NewFixedLengthRandomNameGenerator(3)
475 appName, err := g.Generate()
476 if err != nil {
477 http.Error(w, err.Error(), http.StatusInternalServerError)
478 return
479 }
gio11617ac2024-07-15 16:09:04 +0400480 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400481 if err != nil {
gio33059762024-07-05 13:19:07 +0400482 http.Error(w, err.Error(), http.StatusInternalServerError)
483 return
484 }
gio11617ac2024-07-15 16:09:04 +0400485 if user != "" {
486 http.Error(w, "public key already registered", http.StatusBadRequest)
487 return
488 }
489 user = appName
490 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
491 http.Error(w, err.Error(), http.StatusInternalServerError)
492 return
493 }
494 password := generatePassword()
495 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
496 if err != nil {
497 http.Error(w, err.Error(), http.StatusInternalServerError)
498 return
499 }
500 if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
501 http.Error(w, err.Error(), http.StatusInternalServerError)
502 return
503 }
504 if err := s.st.CreateApp(appName, user); err != nil {
505 http.Error(w, err.Error(), http.StatusInternalServerError)
506 return
507 }
508 if err := s.CreateApp(user, appName, req.AdminPublicKey, req.Network); err != nil {
509 http.Error(w, err.Error(), http.StatusInternalServerError)
510 return
511 }
gio81246f02024-07-10 12:02:15 +0400512 resp := apiCreateAppResp{
513 AppName: appName,
514 Password: password,
515 }
gio33059762024-07-05 13:19:07 +0400516 if err := json.NewEncoder(w).Encode(resp); err != nil {
517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
519 }
520}
521
gio11617ac2024-07-15 16:09:04 +0400522func (s *DodoAppServer) CreateApp(user, appName, adminPublicKey, network string) error {
gio9d66f322024-07-06 13:45:10 +0400523 s.l.Lock()
524 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400525 fmt.Printf("Creating app: %s\n", appName)
526 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400527 return err
gio33059762024-07-05 13:19:07 +0400528 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400529 return nil
gioa60f0de2024-07-08 10:49:48 +0400530 }
gio33059762024-07-05 13:19:07 +0400531 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400532 return err
gio33059762024-07-05 13:19:07 +0400533 }
534 appRepo, err := s.client.GetRepo(appName)
535 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400536 return err
gio33059762024-07-05 13:19:07 +0400537 }
gio11617ac2024-07-15 16:09:04 +0400538 if err := InitRepo(appRepo, network); err != nil {
539 return err
gio33059762024-07-05 13:19:07 +0400540 }
541 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
542 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
543 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400544 return err
gio33059762024-07-05 13:19:07 +0400545 }
546 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
547 suffix, err := suffixGen.Generate()
548 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400549 return err
gio33059762024-07-05 13:19:07 +0400550 }
551 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
552 s.appNs[appName] = namespace
gio11617ac2024-07-15 16:09:04 +0400553 networks, err := s.getNetworks(user)
giocb34ad22024-07-11 08:01:13 +0400554 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400555 return err
giocb34ad22024-07-11 08:01:13 +0400556 }
557 if err := s.updateDodoApp(appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400558 return err
gio33059762024-07-05 13:19:07 +0400559 }
gioa60f0de2024-07-08 10:49:48 +0400560 repo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400561 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400562 return err
gio33059762024-07-05 13:19:07 +0400563 }
564 hf := installer.NewGitHelmFetcher()
565 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
566 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400567 return err
gio33059762024-07-05 13:19:07 +0400568 }
gio9d66f322024-07-06 13:45:10 +0400569 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
570 w, err := fs.Writer(namespacesFile)
571 if err != nil {
572 return "", err
573 }
574 defer w.Close()
575 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
576 return "", err
577 }
578 if _, err := m.Install(
579 app,
580 appName,
581 "/"+appName,
582 namespace,
583 map[string]any{
584 "repoAddr": s.client.GetRepoAddress(appName),
585 "repoHost": strings.Split(s.client.Address(), ":")[0],
586 "gitRepoPublicKey": s.gitRepoPublicKey,
587 },
588 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400589 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400590 installer.WithNoPublish(),
591 installer.WithNoLock(),
592 ); err != nil {
593 return "", err
594 }
595 return fmt.Sprintf("Installed app: %s", appName), nil
596 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400597 return err
gio33059762024-07-05 13:19:07 +0400598 }
599 cfg, err := m.FindInstance(appName)
600 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400601 return err
gio33059762024-07-05 13:19:07 +0400602 }
603 fluxKeys, ok := cfg.Input["fluxKeys"]
604 if !ok {
gio11617ac2024-07-15 16:09:04 +0400605 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400606 }
607 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
608 if !ok {
gio11617ac2024-07-15 16:09:04 +0400609 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400610 }
611 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400612 return err
gio33059762024-07-05 13:19:07 +0400613 } else if ok {
614 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400615 return err
gio33059762024-07-05 13:19:07 +0400616 }
617 } else {
618 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400619 return err
gio33059762024-07-05 13:19:07 +0400620 }
621 }
622 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400623 return err
gio33059762024-07-05 13:19:07 +0400624 }
625 if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400626 return err
gio33059762024-07-05 13:19:07 +0400627 }
gio81246f02024-07-10 12:02:15 +0400628 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400629 return err
gio33059762024-07-05 13:19:07 +0400630 }
gio11617ac2024-07-15 16:09:04 +0400631 return nil
gio33059762024-07-05 13:19:07 +0400632}
633
gio81246f02024-07-10 12:02:15 +0400634type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400635 Public string `json:"public"`
636}
637
gio81246f02024-07-10 12:02:15 +0400638func (s *DodoAppServer) handleApiAddAdminKey(w http.ResponseWriter, r *http.Request) {
639 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400640 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
641 http.Error(w, err.Error(), http.StatusBadRequest)
642 return
643 }
644 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
645 http.Error(w, err.Error(), http.StatusInternalServerError)
646 return
647 }
648}
649
giocb34ad22024-07-11 08:01:13 +0400650func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400651 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400652 if err != nil {
653 return err
654 }
giof8843412024-05-22 16:38:05 +0400655 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400656 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400657 if err != nil {
658 return err
659 }
660 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400661 if err != nil {
662 return err
663 }
664 app, err := installer.NewDodoApp(appCfg)
665 if err != nil {
666 return err
667 }
giof8843412024-05-22 16:38:05 +0400668 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400669 if _, err := m.Install(
670 app,
671 "app",
672 "/.dodo/app",
673 namespace,
674 map[string]any{
gioa60f0de2024-07-08 10:49:48 +0400675 "repoAddr": repo.FullAddress(),
676 "managerAddr": fmt.Sprintf("http://%s", s.self),
677 "appId": name,
678 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400679 },
gio33059762024-07-05 13:19:07 +0400680 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400681 installer.WithNetworks(networks),
giof71a0832024-06-27 14:45:45 +0400682 installer.WithLocalChartGenerator(lg),
683 installer.WithBranch("dodo"),
684 installer.WithForce(),
685 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400686 return err
687 }
688 return nil
689}
gio33059762024-07-05 13:19:07 +0400690
691const goMod = `module dodo.app
692
693go 1.18
694`
695
696const mainGo = `package main
697
698import (
699 "flag"
700 "fmt"
701 "log"
702 "net/http"
703)
704
705var port = flag.Int("port", 8080, "Port to listen on")
706
707func handler(w http.ResponseWriter, r *http.Request) {
708 fmt.Fprintln(w, "Hello from Dodo App!")
709}
710
711func main() {
712 flag.Parse()
713 http.HandleFunc("/", handler)
714 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
715}
716`
717
718const appCue = `app: {
719 type: "golang:1.22.0"
720 run: "main.go"
721 ingress: {
gio23bdc1b2024-07-11 16:07:47 +0400722 network: "%s"
gio33059762024-07-05 13:19:07 +0400723 subdomain: "testapp"
724 auth: enabled: false
725 }
726}
727`
728
gio11617ac2024-07-15 16:09:04 +0400729func InitRepo(repo soft.RepoIO, network string) error {
gio33059762024-07-05 13:19:07 +0400730 return repo.Do(func(fs soft.RepoFS) (string, error) {
731 {
732 w, err := fs.Writer("go.mod")
733 if err != nil {
734 return "", err
735 }
736 defer w.Close()
737 fmt.Fprint(w, goMod)
738 }
739 {
740 w, err := fs.Writer("main.go")
741 if err != nil {
742 return "", err
743 }
744 defer w.Close()
745 fmt.Fprintf(w, "%s", mainGo)
746 }
747 {
748 w, err := fs.Writer("app.cue")
749 if err != nil {
750 return "", err
751 }
752 defer w.Close()
gio11617ac2024-07-15 16:09:04 +0400753 fmt.Fprintf(w, appCue, network)
gio33059762024-07-05 13:19:07 +0400754 }
755 return "go web app template", nil
756 })
757}
gio81246f02024-07-10 12:02:15 +0400758
759func generatePassword() string {
760 return "foo"
761}
giocb34ad22024-07-11 08:01:13 +0400762
gio11617ac2024-07-15 16:09:04 +0400763func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400764 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400765 resp, err := http.Get(addr)
766 if err != nil {
767 return nil, err
768 }
gio23bdc1b2024-07-11 16:07:47 +0400769 networks := []installer.Network{}
770 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400771 return nil, err
772 }
gio11617ac2024-07-15 16:09:04 +0400773 return s.nf.Filter(user, networks)
774}
775
776func pickNetwork(networks []installer.Network, network string) []installer.Network {
777 for _, n := range networks {
778 if n.Name == network {
779 return []installer.Network{n}
780 }
781 }
782 return []installer.Network{}
783}
784
785type NetworkFilter interface {
786 Filter(user string, networks []installer.Network) ([]installer.Network, error)
787}
788
789type noNetworkFilter struct{}
790
791func NewNoNetworkFilter() NetworkFilter {
792 return noNetworkFilter{}
793}
794
795func (f noNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
796 return networks, nil
797}
798
799type filterByOwner struct {
800 st Store
801}
802
803func NewNetworkFilterByOwner(st Store) NetworkFilter {
804 return &filterByOwner{st}
805}
806
807func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
808 network, err := f.st.GetUserNetwork(user)
809 if err != nil {
810 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400811 }
812 ret := []installer.Network{}
813 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400814 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400815 ret = append(ret, n)
816 }
817 }
giocb34ad22024-07-11 08:01:13 +0400818 return ret, nil
819}
gio11617ac2024-07-15 16:09:04 +0400820
821type allowListFilter struct {
822 allowed []string
823}
824
825func NewAllowListFilter(allowed []string) NetworkFilter {
826 return &allowListFilter{allowed}
827}
828
829func (f *allowListFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
830 ret := []installer.Network{}
831 for _, n := range networks {
832 if slices.Contains(f.allowed, n.Name) {
833 ret = append(ret, n)
834 }
835 }
836 return ret, nil
837}
838
839type combinedNetworkFilter struct {
840 filters []NetworkFilter
841}
842
843func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
844 return &combinedNetworkFilter{filters}
845}
846
847func (f *combinedNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
848 ret := networks
849 var err error
850 for _, f := range f.filters {
851 ret, err = f.Filter(app, ret)
852 if err != nil {
853 return nil, err
854 }
855 }
856 return ret, nil
857}