blob: e6f3d3714453e7727ab657213d3ebc6c7c7a401e [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"
gio8fae3af2024-07-25 13:43:31 +040040 apiPublicData = "/api/public-data"
41 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040042 sessionCookie = "dodo-app-session"
43 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040044)
45
gio5e49bb62024-07-20 10:43:19 +040046var types = []string{"golang:1.22.0", "golang:1.20.0", "hugo:latest"}
47
gio23bdc1b2024-07-11 16:07:47 +040048type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040049 index *template.Template
50 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040051}
52
53func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040054 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040055 if err != nil {
56 return dodoAppTmplts{}, err
57 }
gio5e49bb62024-07-20 10:43:19 +040058 parse := func(path string) (*template.Template, error) {
59 if b, err := base.Clone(); err != nil {
60 return nil, err
61 } else {
62 return b.ParseFS(fs, path)
63 }
64 }
65 index, err := parse("dodo-app-tmpl/index.html")
66 if err != nil {
67 return dodoAppTmplts{}, err
68 }
69 appStatus, err := parse("dodo-app-tmpl/app_status.html")
70 if err != nil {
71 return dodoAppTmplts{}, err
72 }
73 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040074}
75
gio0eaf2712024-04-14 13:08:46 +040076type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040077 l sync.Locker
78 st Store
gio11617ac2024-07-15 16:09:04 +040079 nf NetworkFilter
80 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040081 port int
82 apiPort int
83 self string
gio11617ac2024-07-15 16:09:04 +040084 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040085 sshKey string
86 gitRepoPublicKey string
87 client soft.Client
88 namespace string
89 envAppManagerAddr string
90 env installer.EnvConfig
91 nsc installer.NamespaceCreator
92 jc installer.JobCreator
93 workers map[string]map[string]struct{}
94 appNs map[string]string
gio23bdc1b2024-07-11 16:07:47 +040095 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040096 appTmpls AppTmplStore
gio0eaf2712024-04-14 13:08:46 +040097}
98
gio33059762024-07-05 13:19:07 +040099// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400100func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400101 st Store,
gio11617ac2024-07-15 16:09:04 +0400102 nf NetworkFilter,
103 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400104 port int,
gioa60f0de2024-07-08 10:49:48 +0400105 apiPort int,
gio33059762024-07-05 13:19:07 +0400106 self string,
gio11617ac2024-07-15 16:09:04 +0400107 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400108 sshKey string,
gio33059762024-07-05 13:19:07 +0400109 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400110 client soft.Client,
111 namespace string,
giocb34ad22024-07-11 08:01:13 +0400112 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400113 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400114 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400115 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +0400116) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400117 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
118 if err != nil {
119 return nil, err
120 }
gio5e49bb62024-07-20 10:43:19 +0400121 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
122 if err != nil {
123 return nil, err
124 }
125 appTmpls, err := NewAppTmplStoreFS(apps)
126 if err != nil {
127 return nil, err
128 }
gio9d66f322024-07-06 13:45:10 +0400129 s := &DodoAppServer{
130 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400131 st,
gio11617ac2024-07-15 16:09:04 +0400132 nf,
133 ug,
gio0eaf2712024-04-14 13:08:46 +0400134 port,
gioa60f0de2024-07-08 10:49:48 +0400135 apiPort,
gio33059762024-07-05 13:19:07 +0400136 self,
gio11617ac2024-07-15 16:09:04 +0400137 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400138 sshKey,
gio33059762024-07-05 13:19:07 +0400139 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400140 client,
141 namespace,
giocb34ad22024-07-11 08:01:13 +0400142 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400143 env,
gio33059762024-07-05 13:19:07 +0400144 nsc,
giof8843412024-05-22 16:38:05 +0400145 jc,
gio266c04f2024-07-03 14:18:45 +0400146 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +0400147 map[string]string{},
gio23bdc1b2024-07-11 16:07:47 +0400148 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400149 appTmpls,
gio0eaf2712024-04-14 13:08:46 +0400150 }
gioa60f0de2024-07-08 10:49:48 +0400151 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400152 if err != nil {
153 return nil, err
154 }
155 r, err := config.Reader(namespacesFile)
156 if err == nil {
157 defer r.Close()
158 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
159 return nil, err
160 }
161 } else if !errors.Is(err, fs.ErrNotExist) {
162 return nil, err
163 }
164 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400165}
166
167func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400168 e := make(chan error)
169 go func() {
170 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400171 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400172 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400173 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400174 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
175 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400176 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
177 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
178 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
179 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400180 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400181 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
182 }()
183 go func() {
184 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400185 r.HandleFunc("/update", s.handleAPIUpdate)
186 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
187 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400188 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
189 }()
190 return <-e
191}
192
gio11617ac2024-07-15 16:09:04 +0400193type UserGetter interface {
194 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400195 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400196}
197
198type externalUserGetter struct {
199 sc *securecookie.SecureCookie
200}
201
202func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400203 return &externalUserGetter{securecookie.New(
204 securecookie.GenerateRandomKey(64),
205 securecookie.GenerateRandomKey(32),
206 )}
gio11617ac2024-07-15 16:09:04 +0400207}
208
209func (ug *externalUserGetter) Get(r *http.Request) string {
210 cookie, err := r.Cookie(sessionCookie)
211 if err != nil {
212 return ""
213 }
214 var user string
215 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
216 return ""
217 }
218 return user
219}
220
gio8fae3af2024-07-25 13:43:31 +0400221func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
222 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
223 cookie := &http.Cookie{
224 Name: sessionCookie,
225 Value: encoded,
226 Path: "/",
227 Secure: true,
228 HttpOnly: true,
229 }
230 http.SetCookie(w, cookie)
231 return nil
232 } else {
233 return err
234 }
235}
236
gio11617ac2024-07-15 16:09:04 +0400237type internalUserGetter struct{}
238
239func NewInternalUserGetter() UserGetter {
240 return internalUserGetter{}
241}
242
243func (ug internalUserGetter) Get(r *http.Request) string {
244 return r.Header.Get("X-User")
245}
246
gio8fae3af2024-07-25 13:43:31 +0400247func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
248 return nil
249}
250
gio81246f02024-07-10 12:02:15 +0400251func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
252 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400253 if strings.HasSuffix(r.URL.Path, loginPath) ||
254 strings.HasPrefix(r.URL.Path, logoutPath) ||
255 strings.HasPrefix(r.URL.Path, staticPath) ||
256 strings.HasPrefix(r.URL.Path, apiPublicData) ||
257 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400258 next.ServeHTTP(w, r)
259 return
260 }
gio11617ac2024-07-15 16:09:04 +0400261 user := s.ug.Get(r)
262 if user == "" {
gio81246f02024-07-10 12:02:15 +0400263 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 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
270 return
271 }
gio81246f02024-07-10 12:02:15 +0400272 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
273 })
274}
275
276func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400277 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400278 http.SetCookie(w, &http.Cookie{
279 Name: sessionCookie,
280 Value: "",
281 Path: "/",
282 HttpOnly: true,
283 Secure: true,
284 })
285 http.Redirect(w, r, "/", http.StatusSeeOther)
286}
287
288func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
289 vars := mux.Vars(r)
290 appName, ok := vars["app-name"]
291 if !ok || appName == "" {
292 http.Error(w, "missing app-name", http.StatusBadRequest)
293 return
294 }
295 fmt.Fprint(w, `
296<!DOCTYPE html>
297<html lang='en'>
298 <head>
299 <title>dodo: app - login</title>
300 <meta charset='utf-8'>
301 </head>
302 <body>
303 <form action="" method="POST">
304 <input type="password" placeholder="Password" name="password" required />
305 <button type="submit">Login</button>
306 </form>
307 </body>
308</html>
309`)
310}
311
312func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
313 vars := mux.Vars(r)
314 appName, ok := vars["app-name"]
315 if !ok || appName == "" {
316 http.Error(w, "missing app-name", http.StatusBadRequest)
317 return
318 }
319 password := r.FormValue("password")
320 if password == "" {
321 http.Error(w, "missing password", http.StatusBadRequest)
322 return
323 }
324 user, err := s.st.GetAppOwner(appName)
325 if err != nil {
326 http.Error(w, err.Error(), http.StatusInternalServerError)
327 return
328 }
329 hashed, err := s.st.GetUserPassword(user)
330 if err != nil {
331 http.Error(w, err.Error(), http.StatusInternalServerError)
332 return
333 }
334 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
335 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
336 return
337 }
gio8fae3af2024-07-25 13:43:31 +0400338 if err := s.ug.Encode(w, user); err != nil {
339 http.Error(w, err.Error(), http.StatusInternalServerError)
340 return
gio81246f02024-07-10 12:02:15 +0400341 }
342 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
343}
344
gio23bdc1b2024-07-11 16:07:47 +0400345type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400346 Apps []string
347 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400348 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400349}
350
gioa60f0de2024-07-08 10:49:48 +0400351func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400352 user := r.Context().Value(userCtx)
353 if user == nil {
354 http.Error(w, "unauthorized", http.StatusUnauthorized)
355 return
356 }
357 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400358 if err != nil {
359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
361 }
gio11617ac2024-07-15 16:09:04 +0400362 networks, err := s.getNetworks(user.(string))
363 if err != nil {
364 http.Error(w, err.Error(), http.StatusInternalServerError)
365 return
366 }
gio5e49bb62024-07-20 10:43:19 +0400367 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400368 if err := s.tmplts.index.Execute(w, data); err != nil {
369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
gioa60f0de2024-07-08 10:49:48 +0400371 }
372}
373
gio5e49bb62024-07-20 10:43:19 +0400374type appStatusData struct {
375 Name string
376 GitCloneCommand string
377 Commits []Commit
378}
379
gioa60f0de2024-07-08 10:49:48 +0400380func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
381 vars := mux.Vars(r)
382 appName, ok := vars["app-name"]
383 if !ok || appName == "" {
384 http.Error(w, "missing app-name", http.StatusBadRequest)
385 return
386 }
387 commits, err := s.st.GetCommitHistory(appName)
388 if err != nil {
389 http.Error(w, err.Error(), http.StatusInternalServerError)
390 return
391 }
gio5e49bb62024-07-20 10:43:19 +0400392 data := appStatusData{
393 Name: appName,
394 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
395 Commits: commits,
396 }
397 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
398 http.Error(w, err.Error(), http.StatusInternalServerError)
399 return
gioa60f0de2024-07-08 10:49:48 +0400400 }
gio0eaf2712024-04-14 13:08:46 +0400401}
402
gio81246f02024-07-10 12:02:15 +0400403type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400404 Ref string `json:"ref"`
405 Repository struct {
406 Name string `json:"name"`
407 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400408 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400409}
410
gio8fae3af2024-07-25 13:43:31 +0400411func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400412 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400413 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400414 var contents strings.Builder
415 io.Copy(&contents, r.Body)
416 c := contents.String()
417 fmt.Println(c)
418 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400419 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400420 return
421 }
gioa60f0de2024-07-08 10:49:48 +0400422 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400423 return
424 }
gioa60f0de2024-07-08 10:49:48 +0400425 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400426 go func() {
gio11617ac2024-07-15 16:09:04 +0400427 owner, err := s.st.GetAppOwner(req.Repository.Name)
428 if err != nil {
429 return
430 }
431 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400432 if err != nil {
433 return
434 }
435 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400436 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
437 fmt.Printf("Error: %s\n", err.Error())
438 return
439 }
440 }
441 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
442 fmt.Printf("Error: %s\n", err.Error())
443 }
444 for addr, _ := range s.workers[req.Repository.Name] {
445 go func() {
446 // TODO(gio): make port configurable
447 http.Get(fmt.Sprintf("http://%s/update", addr))
448 }()
gio0eaf2712024-04-14 13:08:46 +0400449 }
450 }()
gio0eaf2712024-04-14 13:08:46 +0400451}
452
gio81246f02024-07-10 12:02:15 +0400453type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400454 Address string `json:"address"`
455}
456
gio8fae3af2024-07-25 13:43:31 +0400457func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400458 vars := mux.Vars(r)
459 appName, ok := vars["app-name"]
460 if !ok || appName == "" {
461 http.Error(w, "missing app-name", http.StatusBadRequest)
462 return
463 }
gio81246f02024-07-10 12:02:15 +0400464 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400465 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
466 http.Error(w, err.Error(), http.StatusInternalServerError)
467 return
468 }
gioa60f0de2024-07-08 10:49:48 +0400469 if _, ok := s.workers[appName]; !ok {
470 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400471 }
gioa60f0de2024-07-08 10:49:48 +0400472 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400473}
474
gio11617ac2024-07-15 16:09:04 +0400475func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
476 u := r.Context().Value(userCtx)
477 if u == nil {
478 http.Error(w, "unauthorized", http.StatusUnauthorized)
479 return
480 }
481 user, ok := u.(string)
482 if !ok {
483 http.Error(w, "could not get user", http.StatusInternalServerError)
484 return
485 }
486 network := r.FormValue("network")
487 if network == "" {
488 http.Error(w, "missing network", http.StatusBadRequest)
489 return
490 }
gio5e49bb62024-07-20 10:43:19 +0400491 subdomain := r.FormValue("subdomain")
492 if subdomain == "" {
493 http.Error(w, "missing subdomain", http.StatusBadRequest)
494 return
495 }
496 appType := r.FormValue("type")
497 if appType == "" {
498 http.Error(w, "missing type", http.StatusBadRequest)
499 return
500 }
gio11617ac2024-07-15 16:09:04 +0400501 adminPublicKey := r.FormValue("admin-public-key")
gio5e49bb62024-07-20 10:43:19 +0400502 if adminPublicKey == "" {
gio11617ac2024-07-15 16:09:04 +0400503 http.Error(w, "missing admin public key", http.StatusBadRequest)
504 return
505 }
506 g := installer.NewFixedLengthRandomNameGenerator(3)
507 appName, err := g.Generate()
508 if err != nil {
509 http.Error(w, err.Error(), http.StatusInternalServerError)
510 return
511 }
512 if ok, err := s.client.UserExists(user); err != nil {
513 http.Error(w, err.Error(), http.StatusInternalServerError)
514 return
515 } else if !ok {
516 if err := s.client.AddUser(user, adminPublicKey); err != nil {
517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
519 }
520 }
521 if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
524 }
525 if err := s.st.CreateApp(appName, user); err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
528 }
gio5e49bb62024-07-20 10:43:19 +0400529 if err := s.CreateApp(user, appName, appType, adminPublicKey, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400530 http.Error(w, err.Error(), http.StatusInternalServerError)
531 return
532 }
533 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
534}
535
gio81246f02024-07-10 12:02:15 +0400536type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400537 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400538 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400539 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400540 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400541}
542
gio81246f02024-07-10 12:02:15 +0400543type apiCreateAppResp struct {
544 AppName string `json:"appName"`
545 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400546}
547
gio8fae3af2024-07-25 13:43:31 +0400548func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400549 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400550 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
551 http.Error(w, err.Error(), http.StatusBadRequest)
552 return
553 }
554 g := installer.NewFixedLengthRandomNameGenerator(3)
555 appName, err := g.Generate()
556 if err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
gio11617ac2024-07-15 16:09:04 +0400560 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400561 if err != nil {
gio33059762024-07-05 13:19:07 +0400562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 }
gio11617ac2024-07-15 16:09:04 +0400565 if user != "" {
566 http.Error(w, "public key already registered", http.StatusBadRequest)
567 return
568 }
569 user = appName
570 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
571 http.Error(w, err.Error(), http.StatusInternalServerError)
572 return
573 }
574 password := generatePassword()
575 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
576 if err != nil {
577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580 if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
581 http.Error(w, err.Error(), http.StatusInternalServerError)
582 return
583 }
584 if err := s.st.CreateApp(appName, user); err != nil {
585 http.Error(w, err.Error(), http.StatusInternalServerError)
586 return
587 }
gio5e49bb62024-07-20 10:43:19 +0400588 if err := s.CreateApp(user, appName, req.AppType, req.AdminPublicKey, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400589 http.Error(w, err.Error(), http.StatusInternalServerError)
590 return
591 }
gio81246f02024-07-10 12:02:15 +0400592 resp := apiCreateAppResp{
593 AppName: appName,
594 Password: password,
595 }
gio8fae3af2024-07-25 13:43:31 +0400596 w.Header().Set("Access-Control-Allow-Origin", "*")
gio33059762024-07-05 13:19:07 +0400597 if err := json.NewEncoder(w).Encode(resp); err != nil {
598 http.Error(w, err.Error(), http.StatusInternalServerError)
599 return
600 }
601}
602
gio5e49bb62024-07-20 10:43:19 +0400603func (s *DodoAppServer) CreateApp(user, appName, appType, adminPublicKey, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400604 s.l.Lock()
605 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400606 fmt.Printf("Creating app: %s\n", appName)
607 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400608 return err
gio33059762024-07-05 13:19:07 +0400609 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400610 return nil
gioa60f0de2024-07-08 10:49:48 +0400611 }
gio5e49bb62024-07-20 10:43:19 +0400612 networks, err := s.getNetworks(user)
613 if err != nil {
614 return err
615 }
616 n, ok := installer.NetworkMap(networks)[strings.ToLower(network)]
617 if !ok {
618 return fmt.Errorf("network not found: %s\n", network)
619 }
gio33059762024-07-05 13:19:07 +0400620 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400621 return err
gio33059762024-07-05 13:19:07 +0400622 }
623 appRepo, err := s.client.GetRepo(appName)
624 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400625 return err
gio33059762024-07-05 13:19:07 +0400626 }
gio5e49bb62024-07-20 10:43:19 +0400627 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400628 return err
gio33059762024-07-05 13:19:07 +0400629 }
630 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
631 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
632 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400633 return err
gio33059762024-07-05 13:19:07 +0400634 }
635 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
636 suffix, err := suffixGen.Generate()
637 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400638 return err
gio33059762024-07-05 13:19:07 +0400639 }
640 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
641 s.appNs[appName] = namespace
giocb34ad22024-07-11 08:01:13 +0400642 if err := s.updateDodoApp(appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400643 return err
gio33059762024-07-05 13:19:07 +0400644 }
gioa60f0de2024-07-08 10:49:48 +0400645 repo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400646 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400647 return err
gio33059762024-07-05 13:19:07 +0400648 }
649 hf := installer.NewGitHelmFetcher()
650 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
651 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400652 return err
gio33059762024-07-05 13:19:07 +0400653 }
gio9d66f322024-07-06 13:45:10 +0400654 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
655 w, err := fs.Writer(namespacesFile)
656 if err != nil {
657 return "", err
658 }
659 defer w.Close()
660 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
661 return "", err
662 }
663 if _, err := m.Install(
664 app,
665 appName,
666 "/"+appName,
667 namespace,
668 map[string]any{
669 "repoAddr": s.client.GetRepoAddress(appName),
670 "repoHost": strings.Split(s.client.Address(), ":")[0],
671 "gitRepoPublicKey": s.gitRepoPublicKey,
672 },
673 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400674 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400675 installer.WithNoPublish(),
676 installer.WithNoLock(),
677 ); err != nil {
678 return "", err
679 }
680 return fmt.Sprintf("Installed app: %s", appName), nil
681 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400682 return err
gio33059762024-07-05 13:19:07 +0400683 }
684 cfg, err := m.FindInstance(appName)
685 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400686 return err
gio33059762024-07-05 13:19:07 +0400687 }
688 fluxKeys, ok := cfg.Input["fluxKeys"]
689 if !ok {
gio11617ac2024-07-15 16:09:04 +0400690 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400691 }
692 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
693 if !ok {
gio11617ac2024-07-15 16:09:04 +0400694 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400695 }
696 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400697 return err
gio33059762024-07-05 13:19:07 +0400698 } else if ok {
699 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400700 return err
gio33059762024-07-05 13:19:07 +0400701 }
702 } else {
703 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400704 return err
gio33059762024-07-05 13:19:07 +0400705 }
706 }
707 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400708 return err
gio33059762024-07-05 13:19:07 +0400709 }
710 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 +0400711 return err
gio33059762024-07-05 13:19:07 +0400712 }
gio81246f02024-07-10 12:02:15 +0400713 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400714 return err
gio33059762024-07-05 13:19:07 +0400715 }
gio11617ac2024-07-15 16:09:04 +0400716 return nil
gio33059762024-07-05 13:19:07 +0400717}
718
gio81246f02024-07-10 12:02:15 +0400719type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400720 Public string `json:"public"`
721}
722
gio8fae3af2024-07-25 13:43:31 +0400723func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400724 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400725 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
726 http.Error(w, err.Error(), http.StatusBadRequest)
727 return
728 }
729 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
730 http.Error(w, err.Error(), http.StatusInternalServerError)
731 return
732 }
733}
734
giocb34ad22024-07-11 08:01:13 +0400735func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400736 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400737 if err != nil {
738 return err
739 }
giof8843412024-05-22 16:38:05 +0400740 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400741 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400742 if err != nil {
743 return err
744 }
745 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400746 if err != nil {
747 return err
748 }
749 app, err := installer.NewDodoApp(appCfg)
750 if err != nil {
751 return err
752 }
giof8843412024-05-22 16:38:05 +0400753 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400754 if _, err := m.Install(
755 app,
756 "app",
757 "/.dodo/app",
758 namespace,
759 map[string]any{
gioa60f0de2024-07-08 10:49:48 +0400760 "repoAddr": repo.FullAddress(),
761 "managerAddr": fmt.Sprintf("http://%s", s.self),
762 "appId": name,
763 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400764 },
gio33059762024-07-05 13:19:07 +0400765 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400766 installer.WithNetworks(networks),
giof71a0832024-06-27 14:45:45 +0400767 installer.WithLocalChartGenerator(lg),
768 installer.WithBranch("dodo"),
769 installer.WithForce(),
770 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400771 return err
772 }
773 return nil
774}
gio33059762024-07-05 13:19:07 +0400775
gio5e49bb62024-07-20 10:43:19 +0400776func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
777 appType = strings.ReplaceAll(appType, ":", "-")
778 appTmpl, err := s.appTmpls.Find(appType)
779 if err != nil {
780 return err
gio33059762024-07-05 13:19:07 +0400781 }
gio33059762024-07-05 13:19:07 +0400782 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400783 if err := appTmpl.Render(network, subdomain, repo); err != nil {
784 return "", err
gio33059762024-07-05 13:19:07 +0400785 }
gio5e49bb62024-07-20 10:43:19 +0400786 return "init", nil
gio33059762024-07-05 13:19:07 +0400787 })
788}
gio81246f02024-07-10 12:02:15 +0400789
790func generatePassword() string {
791 return "foo"
792}
giocb34ad22024-07-11 08:01:13 +0400793
gio11617ac2024-07-15 16:09:04 +0400794func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400795 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400796 resp, err := http.Get(addr)
797 if err != nil {
798 return nil, err
799 }
gio23bdc1b2024-07-11 16:07:47 +0400800 networks := []installer.Network{}
801 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400802 return nil, err
803 }
gio11617ac2024-07-15 16:09:04 +0400804 return s.nf.Filter(user, networks)
805}
806
gio8fae3af2024-07-25 13:43:31 +0400807type publicNetworkData struct {
808 Name string `json:"name"`
809 Domain string `json:"domain"`
810}
811
812type publicData struct {
813 Networks []publicNetworkData `json:"networks"`
814 Types []string `json:"types"`
815}
816
817func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
818 networks, err := s.getNetworks("")
819 if err != nil {
820 http.Error(w, err.Error(), http.StatusInternalServerError)
821 return
822 }
823 var ret publicData
824 for _, n := range networks {
825 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
826 }
827 for _, t := range s.appTmpls.Types() {
828 ret.Types = append(ret.Types, strings.ReplaceAll(t, "-", ":"))
829 }
830 w.Header().Set("Access-Control-Allow-Origin", "*")
831 if err := json.NewEncoder(w).Encode(ret); err != nil {
832 http.Error(w, err.Error(), http.StatusInternalServerError)
833 return
834 }
835}
836
gio11617ac2024-07-15 16:09:04 +0400837func pickNetwork(networks []installer.Network, network string) []installer.Network {
838 for _, n := range networks {
839 if n.Name == network {
840 return []installer.Network{n}
841 }
842 }
843 return []installer.Network{}
844}
845
846type NetworkFilter interface {
847 Filter(user string, networks []installer.Network) ([]installer.Network, error)
848}
849
850type noNetworkFilter struct{}
851
852func NewNoNetworkFilter() NetworkFilter {
853 return noNetworkFilter{}
854}
855
gio8fae3af2024-07-25 13:43:31 +0400856func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400857 return networks, nil
858}
859
860type filterByOwner struct {
861 st Store
862}
863
864func NewNetworkFilterByOwner(st Store) NetworkFilter {
865 return &filterByOwner{st}
866}
867
868func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +0400869 if user == "" {
870 return networks, nil
871 }
gio11617ac2024-07-15 16:09:04 +0400872 network, err := f.st.GetUserNetwork(user)
873 if err != nil {
874 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400875 }
876 ret := []installer.Network{}
877 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400878 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400879 ret = append(ret, n)
880 }
881 }
giocb34ad22024-07-11 08:01:13 +0400882 return ret, nil
883}
gio11617ac2024-07-15 16:09:04 +0400884
885type allowListFilter struct {
886 allowed []string
887}
888
889func NewAllowListFilter(allowed []string) NetworkFilter {
890 return &allowListFilter{allowed}
891}
892
gio8fae3af2024-07-25 13:43:31 +0400893func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400894 ret := []installer.Network{}
895 for _, n := range networks {
896 if slices.Contains(f.allowed, n.Name) {
897 ret = append(ret, n)
898 }
899 }
900 return ret, nil
901}
902
903type combinedNetworkFilter struct {
904 filters []NetworkFilter
905}
906
907func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
908 return &combinedNetworkFilter{filters}
909}
910
gio8fae3af2024-07-25 13:43:31 +0400911func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400912 ret := networks
913 var err error
914 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +0400915 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +0400916 if err != nil {
917 return nil, err
918 }
919 }
920 return ret, nil
921}