blob: 4535be2f9615512f522c59337495c472abf39a26 [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
gio5e49bb62024-07-20 10:43:19 +040028//go:embed all:app-tmpl
29var appTmplsFS embed.FS
30
31//go:embed static
32var staticResources embed.FS
33
gio9d66f322024-07-06 13:45:10 +040034const (
gioa60f0de2024-07-08 10:49:48 +040035 ConfigRepoName = "config"
gio9d66f322024-07-06 13:45:10 +040036 namespacesFile = "/namespaces.json"
gio81246f02024-07-10 12:02:15 +040037 loginPath = "/login"
38 logoutPath = "/logout"
gio5e49bb62024-07-20 10:43:19 +040039 staticPath = "/static"
gio81246f02024-07-10 12:02:15 +040040 sessionCookie = "dodo-app-session"
41 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040042)
43
gio5e49bb62024-07-20 10:43:19 +040044var types = []string{"golang:1.22.0", "golang:1.20.0", "hugo:latest"}
45
gio23bdc1b2024-07-11 16:07:47 +040046type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040047 index *template.Template
48 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040049}
50
51func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040052 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040053 if err != nil {
54 return dodoAppTmplts{}, err
55 }
gio5e49bb62024-07-20 10:43:19 +040056 parse := func(path string) (*template.Template, error) {
57 if b, err := base.Clone(); err != nil {
58 return nil, err
59 } else {
60 return b.ParseFS(fs, path)
61 }
62 }
63 index, err := parse("dodo-app-tmpl/index.html")
64 if err != nil {
65 return dodoAppTmplts{}, err
66 }
67 appStatus, err := parse("dodo-app-tmpl/app_status.html")
68 if err != nil {
69 return dodoAppTmplts{}, err
70 }
71 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040072}
73
gio0eaf2712024-04-14 13:08:46 +040074type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040075 l sync.Locker
76 st Store
gio11617ac2024-07-15 16:09:04 +040077 nf NetworkFilter
78 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040079 port int
80 apiPort int
81 self string
gio11617ac2024-07-15 16:09:04 +040082 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040083 sshKey string
84 gitRepoPublicKey string
85 client soft.Client
86 namespace string
87 envAppManagerAddr string
88 env installer.EnvConfig
89 nsc installer.NamespaceCreator
90 jc installer.JobCreator
91 workers map[string]map[string]struct{}
92 appNs map[string]string
93 sc *securecookie.SecureCookie
gio23bdc1b2024-07-11 16:07:47 +040094 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040095 appTmpls AppTmplStore
gio0eaf2712024-04-14 13:08:46 +040096}
97
gio33059762024-07-05 13:19:07 +040098// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040099func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400100 st Store,
gio11617ac2024-07-15 16:09:04 +0400101 nf NetworkFilter,
102 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400103 port int,
gioa60f0de2024-07-08 10:49:48 +0400104 apiPort int,
gio33059762024-07-05 13:19:07 +0400105 self string,
gio11617ac2024-07-15 16:09:04 +0400106 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400107 sshKey string,
gio33059762024-07-05 13:19:07 +0400108 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400109 client soft.Client,
110 namespace string,
giocb34ad22024-07-11 08:01:13 +0400111 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400112 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400113 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400114 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +0400115) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400116 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
117 if err != nil {
118 return nil, err
119 }
gio81246f02024-07-10 12:02:15 +0400120 sc := securecookie.New(
121 securecookie.GenerateRandomKey(64),
122 securecookie.GenerateRandomKey(32),
123 )
gio5e49bb62024-07-20 10:43:19 +0400124 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
125 if err != nil {
126 return nil, err
127 }
128 appTmpls, err := NewAppTmplStoreFS(apps)
129 if err != nil {
130 return nil, err
131 }
gio9d66f322024-07-06 13:45:10 +0400132 s := &DodoAppServer{
133 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400134 st,
gio11617ac2024-07-15 16:09:04 +0400135 nf,
136 ug,
gio0eaf2712024-04-14 13:08:46 +0400137 port,
gioa60f0de2024-07-08 10:49:48 +0400138 apiPort,
gio33059762024-07-05 13:19:07 +0400139 self,
gio11617ac2024-07-15 16:09:04 +0400140 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400141 sshKey,
gio33059762024-07-05 13:19:07 +0400142 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400143 client,
144 namespace,
giocb34ad22024-07-11 08:01:13 +0400145 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400146 env,
gio33059762024-07-05 13:19:07 +0400147 nsc,
giof8843412024-05-22 16:38:05 +0400148 jc,
gio266c04f2024-07-03 14:18:45 +0400149 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +0400150 map[string]string{},
gio81246f02024-07-10 12:02:15 +0400151 sc,
gio23bdc1b2024-07-11 16:07:47 +0400152 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400153 appTmpls,
gio0eaf2712024-04-14 13:08:46 +0400154 }
gioa60f0de2024-07-08 10:49:48 +0400155 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400156 if err != nil {
157 return nil, err
158 }
159 r, err := config.Reader(namespacesFile)
160 if err == nil {
161 defer r.Close()
162 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
163 return nil, err
164 }
165 } else if !errors.Is(err, fs.ErrNotExist) {
166 return nil, err
167 }
168 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400169}
170
171func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400172 e := make(chan error)
173 go func() {
174 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400175 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400176 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400177 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
178 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
179 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
180 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
181 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400182 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400183 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
184 }()
185 go func() {
186 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400187 r.HandleFunc("/update", s.handleApiUpdate)
188 r.HandleFunc("/api/apps/{app-name}/workers", s.handleApiRegisterWorker).Methods(http.MethodPost)
189 r.HandleFunc("/api/apps", s.handleApiCreateApp).Methods(http.MethodPost)
190 r.HandleFunc("/api/add-admin-key", s.handleApiAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400191 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
192 }()
193 return <-e
194}
195
gio11617ac2024-07-15 16:09:04 +0400196type UserGetter interface {
197 Get(r *http.Request) string
198}
199
200type externalUserGetter struct {
201 sc *securecookie.SecureCookie
202}
203
204func NewExternalUserGetter() UserGetter {
205 return &externalUserGetter{}
206}
207
208func (ug *externalUserGetter) Get(r *http.Request) string {
209 cookie, err := r.Cookie(sessionCookie)
210 if err != nil {
211 return ""
212 }
213 var user string
214 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
215 return ""
216 }
217 return user
218}
219
220type internalUserGetter struct{}
221
222func NewInternalUserGetter() UserGetter {
223 return internalUserGetter{}
224}
225
226func (ug internalUserGetter) Get(r *http.Request) string {
227 return r.Header.Get("X-User")
228}
229
gio81246f02024-07-10 12:02:15 +0400230func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
231 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio5e49bb62024-07-20 10:43:19 +0400232 if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) || strings.HasPrefix(r.URL.Path, staticPath) {
gio81246f02024-07-10 12:02:15 +0400233 next.ServeHTTP(w, r)
234 return
235 }
gio11617ac2024-07-15 16:09:04 +0400236 user := s.ug.Get(r)
237 if user == "" {
gio81246f02024-07-10 12:02:15 +0400238 vars := mux.Vars(r)
239 appName, ok := vars["app-name"]
240 if !ok || appName == "" {
241 http.Error(w, "missing app-name", http.StatusBadRequest)
242 return
243 }
244 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
245 return
246 }
gio81246f02024-07-10 12:02:15 +0400247 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
248 })
249}
250
251func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
252 http.SetCookie(w, &http.Cookie{
253 Name: sessionCookie,
254 Value: "",
255 Path: "/",
256 HttpOnly: true,
257 Secure: true,
258 })
259 http.Redirect(w, r, "/", http.StatusSeeOther)
260}
261
262func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
263 vars := mux.Vars(r)
264 appName, ok := vars["app-name"]
265 if !ok || appName == "" {
266 http.Error(w, "missing app-name", http.StatusBadRequest)
267 return
268 }
269 fmt.Fprint(w, `
270<!DOCTYPE html>
271<html lang='en'>
272 <head>
273 <title>dodo: app - login</title>
274 <meta charset='utf-8'>
275 </head>
276 <body>
277 <form action="" method="POST">
278 <input type="password" placeholder="Password" name="password" required />
279 <button type="submit">Login</button>
280 </form>
281 </body>
282</html>
283`)
284}
285
286func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
287 vars := mux.Vars(r)
288 appName, ok := vars["app-name"]
289 if !ok || appName == "" {
290 http.Error(w, "missing app-name", http.StatusBadRequest)
291 return
292 }
293 password := r.FormValue("password")
294 if password == "" {
295 http.Error(w, "missing password", http.StatusBadRequest)
296 return
297 }
298 user, err := s.st.GetAppOwner(appName)
299 if err != nil {
300 http.Error(w, err.Error(), http.StatusInternalServerError)
301 return
302 }
303 hashed, err := s.st.GetUserPassword(user)
304 if err != nil {
305 http.Error(w, err.Error(), http.StatusInternalServerError)
306 return
307 }
308 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
309 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
310 return
311 }
312 if encoded, err := s.sc.Encode(sessionCookie, user); err == nil {
313 cookie := &http.Cookie{
314 Name: sessionCookie,
315 Value: encoded,
316 Path: "/",
317 Secure: true,
318 HttpOnly: true,
319 }
320 http.SetCookie(w, cookie)
321 }
322 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
323}
324
gio23bdc1b2024-07-11 16:07:47 +0400325type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400326 Apps []string
327 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400328 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400329}
330
gioa60f0de2024-07-08 10:49:48 +0400331func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400332 user := r.Context().Value(userCtx)
333 if user == nil {
334 http.Error(w, "unauthorized", http.StatusUnauthorized)
335 return
336 }
337 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400338 if err != nil {
339 http.Error(w, err.Error(), http.StatusInternalServerError)
340 return
341 }
gio11617ac2024-07-15 16:09:04 +0400342 networks, err := s.getNetworks(user.(string))
343 if err != nil {
344 http.Error(w, err.Error(), http.StatusInternalServerError)
345 return
346 }
gio5e49bb62024-07-20 10:43:19 +0400347 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400348 if err := s.tmplts.index.Execute(w, data); err != nil {
349 http.Error(w, err.Error(), http.StatusInternalServerError)
350 return
gioa60f0de2024-07-08 10:49:48 +0400351 }
352}
353
gio5e49bb62024-07-20 10:43:19 +0400354type appStatusData struct {
355 Name string
356 GitCloneCommand string
357 Commits []Commit
358}
359
gioa60f0de2024-07-08 10:49:48 +0400360func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
361 vars := mux.Vars(r)
362 appName, ok := vars["app-name"]
363 if !ok || appName == "" {
364 http.Error(w, "missing app-name", http.StatusBadRequest)
365 return
366 }
367 commits, err := s.st.GetCommitHistory(appName)
368 if err != nil {
369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
371 }
gio5e49bb62024-07-20 10:43:19 +0400372 data := appStatusData{
373 Name: appName,
374 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
375 Commits: commits,
376 }
377 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
378 http.Error(w, err.Error(), http.StatusInternalServerError)
379 return
gioa60f0de2024-07-08 10:49:48 +0400380 }
gio0eaf2712024-04-14 13:08:46 +0400381}
382
gio81246f02024-07-10 12:02:15 +0400383type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400384 Ref string `json:"ref"`
385 Repository struct {
386 Name string `json:"name"`
387 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400388 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400389}
390
gio81246f02024-07-10 12:02:15 +0400391func (s *DodoAppServer) handleApiUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400392 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400393 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400394 var contents strings.Builder
395 io.Copy(&contents, r.Body)
396 c := contents.String()
397 fmt.Println(c)
398 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400399 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400400 return
401 }
gioa60f0de2024-07-08 10:49:48 +0400402 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400403 return
404 }
gioa60f0de2024-07-08 10:49:48 +0400405 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400406 go func() {
gio11617ac2024-07-15 16:09:04 +0400407 owner, err := s.st.GetAppOwner(req.Repository.Name)
408 if err != nil {
409 return
410 }
411 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400412 if err != nil {
413 return
414 }
415 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400416 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
417 fmt.Printf("Error: %s\n", err.Error())
418 return
419 }
420 }
421 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
422 fmt.Printf("Error: %s\n", err.Error())
423 }
424 for addr, _ := range s.workers[req.Repository.Name] {
425 go func() {
426 // TODO(gio): make port configurable
427 http.Get(fmt.Sprintf("http://%s/update", addr))
428 }()
gio0eaf2712024-04-14 13:08:46 +0400429 }
430 }()
gio0eaf2712024-04-14 13:08:46 +0400431}
432
gio81246f02024-07-10 12:02:15 +0400433type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400434 Address string `json:"address"`
435}
436
gio81246f02024-07-10 12:02:15 +0400437func (s *DodoAppServer) handleApiRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400438 vars := mux.Vars(r)
439 appName, ok := vars["app-name"]
440 if !ok || appName == "" {
441 http.Error(w, "missing app-name", http.StatusBadRequest)
442 return
443 }
gio81246f02024-07-10 12:02:15 +0400444 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400445 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
446 http.Error(w, err.Error(), http.StatusInternalServerError)
447 return
448 }
gioa60f0de2024-07-08 10:49:48 +0400449 if _, ok := s.workers[appName]; !ok {
450 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400451 }
gioa60f0de2024-07-08 10:49:48 +0400452 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400453}
454
gio11617ac2024-07-15 16:09:04 +0400455func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
456 u := r.Context().Value(userCtx)
457 if u == nil {
458 http.Error(w, "unauthorized", http.StatusUnauthorized)
459 return
460 }
461 user, ok := u.(string)
462 if !ok {
463 http.Error(w, "could not get user", http.StatusInternalServerError)
464 return
465 }
466 network := r.FormValue("network")
467 if network == "" {
468 http.Error(w, "missing network", http.StatusBadRequest)
469 return
470 }
gio5e49bb62024-07-20 10:43:19 +0400471 subdomain := r.FormValue("subdomain")
472 if subdomain == "" {
473 http.Error(w, "missing subdomain", http.StatusBadRequest)
474 return
475 }
476 appType := r.FormValue("type")
477 if appType == "" {
478 http.Error(w, "missing type", http.StatusBadRequest)
479 return
480 }
gio11617ac2024-07-15 16:09:04 +0400481 adminPublicKey := r.FormValue("admin-public-key")
gio5e49bb62024-07-20 10:43:19 +0400482 if adminPublicKey == "" {
gio11617ac2024-07-15 16:09:04 +0400483 http.Error(w, "missing admin public key", http.StatusBadRequest)
484 return
485 }
486 g := installer.NewFixedLengthRandomNameGenerator(3)
487 appName, err := g.Generate()
488 if err != nil {
489 http.Error(w, err.Error(), http.StatusInternalServerError)
490 return
491 }
492 if ok, err := s.client.UserExists(user); err != nil {
493 http.Error(w, err.Error(), http.StatusInternalServerError)
494 return
495 } else if !ok {
496 if err := s.client.AddUser(user, adminPublicKey); err != nil {
497 http.Error(w, err.Error(), http.StatusInternalServerError)
498 return
499 }
500 }
501 if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
502 http.Error(w, err.Error(), http.StatusInternalServerError)
503 return
504 }
505 if err := s.st.CreateApp(appName, user); err != nil {
506 http.Error(w, err.Error(), http.StatusInternalServerError)
507 return
508 }
gio5e49bb62024-07-20 10:43:19 +0400509 if err := s.CreateApp(user, appName, appType, adminPublicKey, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return
512 }
513 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
514}
515
gio81246f02024-07-10 12:02:15 +0400516type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400517 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400518 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400519 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400520 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400521}
522
gio81246f02024-07-10 12:02:15 +0400523type apiCreateAppResp struct {
524 AppName string `json:"appName"`
525 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400526}
527
gio81246f02024-07-10 12:02:15 +0400528func (s *DodoAppServer) handleApiCreateApp(w http.ResponseWriter, r *http.Request) {
529 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400530 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
531 http.Error(w, err.Error(), http.StatusBadRequest)
532 return
533 }
534 g := installer.NewFixedLengthRandomNameGenerator(3)
535 appName, err := g.Generate()
536 if err != nil {
537 http.Error(w, err.Error(), http.StatusInternalServerError)
538 return
539 }
gio11617ac2024-07-15 16:09:04 +0400540 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400541 if err != nil {
gio33059762024-07-05 13:19:07 +0400542 http.Error(w, err.Error(), http.StatusInternalServerError)
543 return
544 }
gio11617ac2024-07-15 16:09:04 +0400545 if user != "" {
546 http.Error(w, "public key already registered", http.StatusBadRequest)
547 return
548 }
549 user = appName
550 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
551 http.Error(w, err.Error(), http.StatusInternalServerError)
552 return
553 }
554 password := generatePassword()
555 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
556 if err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
560 if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
561 http.Error(w, err.Error(), http.StatusInternalServerError)
562 return
563 }
564 if err := s.st.CreateApp(appName, user); err != nil {
565 http.Error(w, err.Error(), http.StatusInternalServerError)
566 return
567 }
gio5e49bb62024-07-20 10:43:19 +0400568 if err := s.CreateApp(user, appName, req.AppType, req.AdminPublicKey, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
571 }
gio81246f02024-07-10 12:02:15 +0400572 resp := apiCreateAppResp{
573 AppName: appName,
574 Password: password,
575 }
gio33059762024-07-05 13:19:07 +0400576 if err := json.NewEncoder(w).Encode(resp); err != nil {
577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580}
581
gio5e49bb62024-07-20 10:43:19 +0400582func (s *DodoAppServer) CreateApp(user, appName, appType, adminPublicKey, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400583 s.l.Lock()
584 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400585 fmt.Printf("Creating app: %s\n", appName)
586 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400587 return err
gio33059762024-07-05 13:19:07 +0400588 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400589 return nil
gioa60f0de2024-07-08 10:49:48 +0400590 }
gio5e49bb62024-07-20 10:43:19 +0400591 networks, err := s.getNetworks(user)
592 if err != nil {
593 return err
594 }
595 n, ok := installer.NetworkMap(networks)[strings.ToLower(network)]
596 if !ok {
597 return fmt.Errorf("network not found: %s\n", network)
598 }
gio33059762024-07-05 13:19:07 +0400599 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400600 return err
gio33059762024-07-05 13:19:07 +0400601 }
602 appRepo, err := s.client.GetRepo(appName)
603 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400604 return err
gio33059762024-07-05 13:19:07 +0400605 }
gio5e49bb62024-07-20 10:43:19 +0400606 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400607 return err
gio33059762024-07-05 13:19:07 +0400608 }
609 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
610 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
611 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400612 return err
gio33059762024-07-05 13:19:07 +0400613 }
614 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
615 suffix, err := suffixGen.Generate()
616 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400617 return err
gio33059762024-07-05 13:19:07 +0400618 }
619 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
620 s.appNs[appName] = namespace
giocb34ad22024-07-11 08:01:13 +0400621 if err := s.updateDodoApp(appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400622 return err
gio33059762024-07-05 13:19:07 +0400623 }
gioa60f0de2024-07-08 10:49:48 +0400624 repo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400625 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400626 return err
gio33059762024-07-05 13:19:07 +0400627 }
628 hf := installer.NewGitHelmFetcher()
629 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
630 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400631 return err
gio33059762024-07-05 13:19:07 +0400632 }
gio9d66f322024-07-06 13:45:10 +0400633 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
634 w, err := fs.Writer(namespacesFile)
635 if err != nil {
636 return "", err
637 }
638 defer w.Close()
639 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
640 return "", err
641 }
642 if _, err := m.Install(
643 app,
644 appName,
645 "/"+appName,
646 namespace,
647 map[string]any{
648 "repoAddr": s.client.GetRepoAddress(appName),
649 "repoHost": strings.Split(s.client.Address(), ":")[0],
650 "gitRepoPublicKey": s.gitRepoPublicKey,
651 },
652 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400653 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400654 installer.WithNoPublish(),
655 installer.WithNoLock(),
656 ); err != nil {
657 return "", err
658 }
659 return fmt.Sprintf("Installed app: %s", appName), nil
660 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400661 return err
gio33059762024-07-05 13:19:07 +0400662 }
663 cfg, err := m.FindInstance(appName)
664 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400665 return err
gio33059762024-07-05 13:19:07 +0400666 }
667 fluxKeys, ok := cfg.Input["fluxKeys"]
668 if !ok {
gio11617ac2024-07-15 16:09:04 +0400669 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400670 }
671 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
672 if !ok {
gio11617ac2024-07-15 16:09:04 +0400673 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400674 }
675 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400676 return err
gio33059762024-07-05 13:19:07 +0400677 } else if ok {
678 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400679 return err
gio33059762024-07-05 13:19:07 +0400680 }
681 } else {
682 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400683 return err
gio33059762024-07-05 13:19:07 +0400684 }
685 }
686 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400687 return err
gio33059762024-07-05 13:19:07 +0400688 }
689 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 +0400690 return err
gio33059762024-07-05 13:19:07 +0400691 }
gio81246f02024-07-10 12:02:15 +0400692 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400693 return err
gio33059762024-07-05 13:19:07 +0400694 }
gio11617ac2024-07-15 16:09:04 +0400695 return nil
gio33059762024-07-05 13:19:07 +0400696}
697
gio81246f02024-07-10 12:02:15 +0400698type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400699 Public string `json:"public"`
700}
701
gio81246f02024-07-10 12:02:15 +0400702func (s *DodoAppServer) handleApiAddAdminKey(w http.ResponseWriter, r *http.Request) {
703 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400704 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
705 http.Error(w, err.Error(), http.StatusBadRequest)
706 return
707 }
708 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
709 http.Error(w, err.Error(), http.StatusInternalServerError)
710 return
711 }
712}
713
giocb34ad22024-07-11 08:01:13 +0400714func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400715 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400716 if err != nil {
717 return err
718 }
giof8843412024-05-22 16:38:05 +0400719 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400720 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400721 if err != nil {
722 return err
723 }
724 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400725 if err != nil {
726 return err
727 }
728 app, err := installer.NewDodoApp(appCfg)
729 if err != nil {
730 return err
731 }
giof8843412024-05-22 16:38:05 +0400732 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400733 if _, err := m.Install(
734 app,
735 "app",
736 "/.dodo/app",
737 namespace,
738 map[string]any{
gioa60f0de2024-07-08 10:49:48 +0400739 "repoAddr": repo.FullAddress(),
740 "managerAddr": fmt.Sprintf("http://%s", s.self),
741 "appId": name,
742 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400743 },
gio33059762024-07-05 13:19:07 +0400744 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400745 installer.WithNetworks(networks),
giof71a0832024-06-27 14:45:45 +0400746 installer.WithLocalChartGenerator(lg),
747 installer.WithBranch("dodo"),
748 installer.WithForce(),
749 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400750 return err
751 }
752 return nil
753}
gio33059762024-07-05 13:19:07 +0400754
gio5e49bb62024-07-20 10:43:19 +0400755func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
756 appType = strings.ReplaceAll(appType, ":", "-")
757 appTmpl, err := s.appTmpls.Find(appType)
758 if err != nil {
759 return err
gio33059762024-07-05 13:19:07 +0400760 }
gio33059762024-07-05 13:19:07 +0400761 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400762 if err := appTmpl.Render(network, subdomain, repo); err != nil {
763 return "", err
gio33059762024-07-05 13:19:07 +0400764 }
gio5e49bb62024-07-20 10:43:19 +0400765 return "init", nil
gio33059762024-07-05 13:19:07 +0400766 })
767}
gio81246f02024-07-10 12:02:15 +0400768
769func generatePassword() string {
770 return "foo"
771}
giocb34ad22024-07-11 08:01:13 +0400772
gio11617ac2024-07-15 16:09:04 +0400773func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400774 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400775 resp, err := http.Get(addr)
776 if err != nil {
777 return nil, err
778 }
gio23bdc1b2024-07-11 16:07:47 +0400779 networks := []installer.Network{}
780 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400781 return nil, err
782 }
gio11617ac2024-07-15 16:09:04 +0400783 return s.nf.Filter(user, networks)
784}
785
786func pickNetwork(networks []installer.Network, network string) []installer.Network {
787 for _, n := range networks {
788 if n.Name == network {
789 return []installer.Network{n}
790 }
791 }
792 return []installer.Network{}
793}
794
795type NetworkFilter interface {
796 Filter(user string, networks []installer.Network) ([]installer.Network, error)
797}
798
799type noNetworkFilter struct{}
800
801func NewNoNetworkFilter() NetworkFilter {
802 return noNetworkFilter{}
803}
804
805func (f noNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
806 return networks, nil
807}
808
809type filterByOwner struct {
810 st Store
811}
812
813func NewNetworkFilterByOwner(st Store) NetworkFilter {
814 return &filterByOwner{st}
815}
816
817func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
818 network, err := f.st.GetUserNetwork(user)
819 if err != nil {
820 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400821 }
822 ret := []installer.Network{}
823 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400824 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400825 ret = append(ret, n)
826 }
827 }
giocb34ad22024-07-11 08:01:13 +0400828 return ret, nil
829}
gio11617ac2024-07-15 16:09:04 +0400830
831type allowListFilter struct {
832 allowed []string
833}
834
835func NewAllowListFilter(allowed []string) NetworkFilter {
836 return &allowListFilter{allowed}
837}
838
839func (f *allowListFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
840 ret := []installer.Network{}
841 for _, n := range networks {
842 if slices.Contains(f.allowed, n.Name) {
843 ret = append(ret, n)
844 }
845 }
846 return ret, nil
847}
848
849type combinedNetworkFilter struct {
850 filters []NetworkFilter
851}
852
853func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
854 return &combinedNetworkFilter{filters}
855}
856
857func (f *combinedNetworkFilter) Filter(app string, networks []installer.Network) ([]installer.Network, error) {
858 ret := networks
859 var err error
860 for _, f := range f.filters {
861 ret, err = f.Filter(app, ret)
862 if err != nil {
863 return nil, err
864 }
865 }
866 return ret, nil
867}