blob: fbf732b02adbc5f5d52aacdc966780750b955fcc [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
gio94904702024-07-26 16:58:34 +04004 "bytes"
gio81246f02024-07-10 12:02:15 +04005 "context"
gio23bdc1b2024-07-11 16:07:47 +04006 "embed"
gio0eaf2712024-04-14 13:08:46 +04007 "encoding/json"
gio9d66f322024-07-06 13:45:10 +04008 "errors"
gio0eaf2712024-04-14 13:08:46 +04009 "fmt"
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"
giocafd4e62024-07-31 10:53:40 +040017 "time"
gio0eaf2712024-04-14 13:08:46 +040018
Davit Tabidzea5ea5092024-08-01 15:28:09 +040019 "golang.org/x/crypto/bcrypt"
20 "golang.org/x/exp/rand"
21
gio0eaf2712024-04-14 13:08:46 +040022 "github.com/giolekva/pcloud/core/installer"
23 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040024
25 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040026 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040027)
28
gio23bdc1b2024-07-11 16:07:47 +040029//go:embed dodo-app-tmpl/*
30var dodoAppTmplFS embed.FS
31
gio5e49bb62024-07-20 10:43:19 +040032//go:embed all:app-tmpl
33var appTmplsFS embed.FS
34
gio9d66f322024-07-06 13:45:10 +040035const (
gioa60f0de2024-07-08 10:49:48 +040036 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040037 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040038 loginPath = "/login"
39 logoutPath = "/logout"
gio1bf00802024-08-17 12:31:41 +040040 staticPath = "/stat/"
gio8fae3af2024-07-25 13:43:31 +040041 apiPublicData = "/api/public-data"
42 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040043 sessionCookie = "dodo-app-session"
44 userCtx = "user"
giob4a3a192024-08-19 09:55:47 +040045 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040046)
47
gio23bdc1b2024-07-11 16:07:47 +040048type dodoAppTmplts struct {
giob4a3a192024-08-19 09:55:47 +040049 index *template.Template
50 appStatus *template.Template
51 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040052 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040053}
54
55func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040056 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040057 if err != nil {
58 return dodoAppTmplts{}, err
59 }
gio5e49bb62024-07-20 10:43:19 +040060 parse := func(path string) (*template.Template, error) {
61 if b, err := base.Clone(); err != nil {
62 return nil, err
63 } else {
64 return b.ParseFS(fs, path)
65 }
66 }
67 index, err := parse("dodo-app-tmpl/index.html")
68 if err != nil {
69 return dodoAppTmplts{}, err
70 }
71 appStatus, err := parse("dodo-app-tmpl/app_status.html")
72 if err != nil {
73 return dodoAppTmplts{}, err
74 }
giob4a3a192024-08-19 09:55:47 +040075 commitStatus, err := parse("dodo-app-tmpl/commit_status.html")
76 if err != nil {
77 return dodoAppTmplts{}, err
78 }
gio183e8342024-08-20 06:01:24 +040079 logs, err := parse("dodo-app-tmpl/logs.html")
80 if err != nil {
81 return dodoAppTmplts{}, err
82 }
83 return dodoAppTmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040084}
85
gio0eaf2712024-04-14 13:08:46 +040086type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040087 l sync.Locker
88 st Store
gio11617ac2024-07-15 16:09:04 +040089 nf NetworkFilter
90 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040091 port int
92 apiPort int
93 self string
gio11617ac2024-07-15 16:09:04 +040094 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040095 sshKey string
96 gitRepoPublicKey string
97 client soft.Client
98 namespace string
99 envAppManagerAddr string
100 env installer.EnvConfig
101 nsc installer.NamespaceCreator
102 jc installer.JobCreator
103 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400104 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +0400105 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +0400106 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400107 external bool
108 fetchUsersAddr string
gio183e8342024-08-20 06:01:24 +0400109 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400110}
111
112type appConfig struct {
113 Namespace string `json:"namespace"`
114 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400115}
116
gio33059762024-07-05 13:19:07 +0400117// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400118func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400119 st Store,
gio11617ac2024-07-15 16:09:04 +0400120 nf NetworkFilter,
121 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400122 port int,
gioa60f0de2024-07-08 10:49:48 +0400123 apiPort int,
gio33059762024-07-05 13:19:07 +0400124 self string,
gio11617ac2024-07-15 16:09:04 +0400125 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400126 sshKey string,
gio33059762024-07-05 13:19:07 +0400127 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400128 client soft.Client,
129 namespace string,
giocb34ad22024-07-11 08:01:13 +0400130 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400131 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400132 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400133 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400134 external bool,
135 fetchUsersAddr string,
gio9d66f322024-07-06 13:45:10 +0400136) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400137 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
138 if err != nil {
139 return nil, err
140 }
gio5e49bb62024-07-20 10:43:19 +0400141 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
142 if err != nil {
143 return nil, err
144 }
145 appTmpls, err := NewAppTmplStoreFS(apps)
146 if err != nil {
147 return nil, err
148 }
gio9d66f322024-07-06 13:45:10 +0400149 s := &DodoAppServer{
150 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400151 st,
gio11617ac2024-07-15 16:09:04 +0400152 nf,
153 ug,
gio0eaf2712024-04-14 13:08:46 +0400154 port,
gioa60f0de2024-07-08 10:49:48 +0400155 apiPort,
gio33059762024-07-05 13:19:07 +0400156 self,
gio11617ac2024-07-15 16:09:04 +0400157 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400158 sshKey,
gio33059762024-07-05 13:19:07 +0400159 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400160 client,
161 namespace,
giocb34ad22024-07-11 08:01:13 +0400162 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400163 env,
gio33059762024-07-05 13:19:07 +0400164 nsc,
giof8843412024-05-22 16:38:05 +0400165 jc,
gio266c04f2024-07-03 14:18:45 +0400166 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400167 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400168 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400169 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400170 external,
171 fetchUsersAddr,
gio183e8342024-08-20 06:01:24 +0400172 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400173 }
gioa60f0de2024-07-08 10:49:48 +0400174 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400175 if err != nil {
176 return nil, err
177 }
giod8ab4f52024-07-26 16:58:34 +0400178 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400179 if err == nil {
180 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400181 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400182 return nil, err
183 }
184 } else if !errors.Is(err, fs.ErrNotExist) {
185 return nil, err
186 }
187 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400188}
189
190func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400191 e := make(chan error)
192 go func() {
193 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400194 r.Use(s.mwAuth)
gio1bf00802024-08-17 12:31:41 +0400195 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400196 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400197 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
198 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400199 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
200 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400201 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400202 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio81246f02024-07-10 12:02:15 +0400203 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
204 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400205 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400206 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
207 }()
208 go func() {
209 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400210 r.HandleFunc("/update", s.handleAPIUpdate)
211 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
212 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
giocafd4e62024-07-31 10:53:40 +0400213 if !s.external {
214 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
215 }
gioa60f0de2024-07-08 10:49:48 +0400216 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
217 }()
giocafd4e62024-07-31 10:53:40 +0400218 if !s.external {
219 go func() {
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400220 rand.Seed(uint64(time.Now().UnixNano()))
giocafd4e62024-07-31 10:53:40 +0400221 s.syncUsers()
222 // TODO(dtabidze): every sync delay should be randomized to avoid all client
223 // applications hitting memberships service at the same time.
224 // For every next sync new delay should be randomly generated from scratch.
225 // We can choose random delay from 1 to 2 minutes.
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400226 // for range time.Tick(1 * time.Minute) {
227 // s.syncUsers()
228 // }
229 for {
230 delay := time.Duration(rand.Intn(60)+60) * time.Second
231 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400232 s.syncUsers()
233 }
234 }()
235 }
gioa60f0de2024-07-08 10:49:48 +0400236 return <-e
237}
238
gio11617ac2024-07-15 16:09:04 +0400239type UserGetter interface {
240 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400241 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400242}
243
244type externalUserGetter struct {
245 sc *securecookie.SecureCookie
246}
247
248func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400249 return &externalUserGetter{securecookie.New(
250 securecookie.GenerateRandomKey(64),
251 securecookie.GenerateRandomKey(32),
252 )}
gio11617ac2024-07-15 16:09:04 +0400253}
254
255func (ug *externalUserGetter) Get(r *http.Request) string {
256 cookie, err := r.Cookie(sessionCookie)
257 if err != nil {
258 return ""
259 }
260 var user string
261 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
262 return ""
263 }
264 return user
265}
266
gio8fae3af2024-07-25 13:43:31 +0400267func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
268 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
269 cookie := &http.Cookie{
270 Name: sessionCookie,
271 Value: encoded,
272 Path: "/",
273 Secure: true,
274 HttpOnly: true,
275 }
276 http.SetCookie(w, cookie)
277 return nil
278 } else {
279 return err
280 }
281}
282
gio11617ac2024-07-15 16:09:04 +0400283type internalUserGetter struct{}
284
285func NewInternalUserGetter() UserGetter {
286 return internalUserGetter{}
287}
288
289func (ug internalUserGetter) Get(r *http.Request) string {
290 return r.Header.Get("X-User")
291}
292
gio8fae3af2024-07-25 13:43:31 +0400293func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
294 return nil
295}
296
gio81246f02024-07-10 12:02:15 +0400297func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
298 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400299 if strings.HasSuffix(r.URL.Path, loginPath) ||
300 strings.HasPrefix(r.URL.Path, logoutPath) ||
301 strings.HasPrefix(r.URL.Path, staticPath) ||
302 strings.HasPrefix(r.URL.Path, apiPublicData) ||
303 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400304 next.ServeHTTP(w, r)
305 return
306 }
gio11617ac2024-07-15 16:09:04 +0400307 user := s.ug.Get(r)
308 if user == "" {
gio81246f02024-07-10 12:02:15 +0400309 vars := mux.Vars(r)
310 appName, ok := vars["app-name"]
311 if !ok || appName == "" {
312 http.Error(w, "missing app-name", http.StatusBadRequest)
313 return
314 }
315 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
316 return
317 }
gio81246f02024-07-10 12:02:15 +0400318 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
319 })
320}
321
322func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400323 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400324 http.SetCookie(w, &http.Cookie{
325 Name: sessionCookie,
326 Value: "",
327 Path: "/",
328 HttpOnly: true,
329 Secure: true,
330 })
331 http.Redirect(w, r, "/", http.StatusSeeOther)
332}
333
334func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
335 vars := mux.Vars(r)
336 appName, ok := vars["app-name"]
337 if !ok || appName == "" {
338 http.Error(w, "missing app-name", http.StatusBadRequest)
339 return
340 }
341 fmt.Fprint(w, `
342<!DOCTYPE html>
343<html lang='en'>
344 <head>
345 <title>dodo: app - login</title>
346 <meta charset='utf-8'>
347 </head>
348 <body>
349 <form action="" method="POST">
350 <input type="password" placeholder="Password" name="password" required />
351 <button type="submit">Login</button>
352 </form>
353 </body>
354</html>
355`)
356}
357
358func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
359 vars := mux.Vars(r)
360 appName, ok := vars["app-name"]
361 if !ok || appName == "" {
362 http.Error(w, "missing app-name", http.StatusBadRequest)
363 return
364 }
365 password := r.FormValue("password")
366 if password == "" {
367 http.Error(w, "missing password", http.StatusBadRequest)
368 return
369 }
370 user, err := s.st.GetAppOwner(appName)
371 if err != nil {
372 http.Error(w, err.Error(), http.StatusInternalServerError)
373 return
374 }
375 hashed, err := s.st.GetUserPassword(user)
376 if err != nil {
377 http.Error(w, err.Error(), http.StatusInternalServerError)
378 return
379 }
380 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
381 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
382 return
383 }
gio8fae3af2024-07-25 13:43:31 +0400384 if err := s.ug.Encode(w, user); err != nil {
385 http.Error(w, err.Error(), http.StatusInternalServerError)
386 return
gio81246f02024-07-10 12:02:15 +0400387 }
388 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
389}
390
giob4a3a192024-08-19 09:55:47 +0400391type navItem struct {
392 Name string
393 Address string
394}
395
gio23bdc1b2024-07-11 16:07:47 +0400396type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400397 Navigation []navItem
398 Apps []string
399 Networks []installer.Network
400 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400401}
402
gioa60f0de2024-07-08 10:49:48 +0400403func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400404 user := r.Context().Value(userCtx)
405 if user == nil {
406 http.Error(w, "unauthorized", http.StatusUnauthorized)
407 return
408 }
409 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400410 if err != nil {
411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
413 }
gio11617ac2024-07-15 16:09:04 +0400414 networks, err := s.getNetworks(user.(string))
415 if err != nil {
416 http.Error(w, err.Error(), http.StatusInternalServerError)
417 return
418 }
giob54db242024-07-30 18:49:33 +0400419 var types []string
420 for _, t := range s.appTmpls.Types() {
421 types = append(types, strings.Replace(t, "-", ":", 1))
422 }
giob4a3a192024-08-19 09:55:47 +0400423 n := []navItem{navItem{"Home", "/"}}
424 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400425 if err := s.tmplts.index.Execute(w, data); err != nil {
426 http.Error(w, err.Error(), http.StatusInternalServerError)
427 return
gioa60f0de2024-07-08 10:49:48 +0400428 }
429}
430
gio5e49bb62024-07-20 10:43:19 +0400431type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400432 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400433 Name string
434 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400435 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400436 LastCommit resourceData
gio5e49bb62024-07-20 10:43:19 +0400437}
438
gioa60f0de2024-07-08 10:49:48 +0400439func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
440 vars := mux.Vars(r)
441 appName, ok := vars["app-name"]
442 if !ok || appName == "" {
443 http.Error(w, "missing app-name", http.StatusBadRequest)
444 return
445 }
gio94904702024-07-26 16:58:34 +0400446 u := r.Context().Value(userCtx)
447 if u == nil {
448 http.Error(w, "unauthorized", http.StatusUnauthorized)
449 return
450 }
451 user, ok := u.(string)
452 if !ok {
453 http.Error(w, "could not get user", http.StatusInternalServerError)
454 return
455 }
456 owner, err := s.st.GetAppOwner(appName)
457 if err != nil {
458 http.Error(w, err.Error(), http.StatusInternalServerError)
459 return
460 }
461 if owner != user {
462 http.Error(w, "unauthorized", http.StatusUnauthorized)
463 return
464 }
gioa60f0de2024-07-08 10:49:48 +0400465 commits, err := s.st.GetCommitHistory(appName)
466 if err != nil {
467 http.Error(w, err.Error(), http.StatusInternalServerError)
468 return
469 }
gio183e8342024-08-20 06:01:24 +0400470 var lastCommitResources resourceData
471 if len(commits) > 0 {
472 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
473 if err != nil {
474 http.Error(w, err.Error(), http.StatusInternalServerError)
475 return
476 }
477 r, err := extractResourceData(lastCommit.Resources.Helm)
478 if err != nil {
479 http.Error(w, err.Error(), http.StatusInternalServerError)
480 return
481 }
482 lastCommitResources = r
483 }
gio5e49bb62024-07-20 10:43:19 +0400484 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400485 Navigation: []navItem{
486 navItem{"Home", "/"},
487 navItem{appName, "/" + appName},
488 },
gio5e49bb62024-07-20 10:43:19 +0400489 Name: appName,
490 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
491 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400492 LastCommit: lastCommitResources,
gio5e49bb62024-07-20 10:43:19 +0400493 }
494 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
495 http.Error(w, err.Error(), http.StatusInternalServerError)
496 return
gioa60f0de2024-07-08 10:49:48 +0400497 }
gio0eaf2712024-04-14 13:08:46 +0400498}
499
giob4a3a192024-08-19 09:55:47 +0400500type volume struct {
501 Name string
502 Size string
503}
504
505type postgresql struct {
506 Name string
507 Version string
508 Volume string
509}
510
511type ingress struct {
512 Host string
513}
514
515type resourceData struct {
516 Volume []volume
517 PostgreSQL []postgresql
518 Ingress []ingress
519}
520
521type commitStatusData struct {
522 Navigation []navItem
523 AppName string
524 Commit Commit
525 Resources resourceData
526}
527
528func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
529 vars := mux.Vars(r)
530 appName, ok := vars["app-name"]
531 if !ok || appName == "" {
532 http.Error(w, "missing app-name", http.StatusBadRequest)
533 return
534 }
535 hash, ok := vars["hash"]
536 if !ok || appName == "" {
537 http.Error(w, "missing app-name", http.StatusBadRequest)
538 return
539 }
540 u := r.Context().Value(userCtx)
541 if u == nil {
542 http.Error(w, "unauthorized", http.StatusUnauthorized)
543 return
544 }
545 user, ok := u.(string)
546 if !ok {
547 http.Error(w, "could not get user", http.StatusInternalServerError)
548 return
549 }
550 owner, err := s.st.GetAppOwner(appName)
551 if err != nil {
552 http.Error(w, err.Error(), http.StatusInternalServerError)
553 return
554 }
555 if owner != user {
556 http.Error(w, "unauthorized", http.StatusUnauthorized)
557 return
558 }
559 commit, err := s.st.GetCommit(hash)
560 if err != nil {
561 // TODO(gio): not-found ?
562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 }
565 var res strings.Builder
566 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
567 http.Error(w, err.Error(), http.StatusInternalServerError)
568 return
569 }
570 resData, err := extractResourceData(commit.Resources.Helm)
571 if err != nil {
572 http.Error(w, err.Error(), http.StatusInternalServerError)
573 return
574 }
575 data := commitStatusData{
576 Navigation: []navItem{
577 navItem{"Home", "/"},
578 navItem{appName, "/" + appName},
579 navItem{hash, "/" + appName + "/" + hash},
580 },
581 AppName: appName,
582 Commit: commit,
583 Resources: resData,
584 }
585 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
586 http.Error(w, err.Error(), http.StatusInternalServerError)
587 return
588 }
589}
590
gio183e8342024-08-20 06:01:24 +0400591type logData struct {
592 Navigation []navItem
593 AppName string
594 Logs template.HTML
595}
596
597func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
598 vars := mux.Vars(r)
599 appName, ok := vars["app-name"]
600 if !ok || appName == "" {
601 http.Error(w, "missing app-name", http.StatusBadRequest)
602 return
603 }
604 u := r.Context().Value(userCtx)
605 if u == nil {
606 http.Error(w, "unauthorized", http.StatusUnauthorized)
607 return
608 }
609 user, ok := u.(string)
610 if !ok {
611 http.Error(w, "could not get user", http.StatusInternalServerError)
612 return
613 }
614 owner, err := s.st.GetAppOwner(appName)
615 if err != nil {
616 http.Error(w, err.Error(), http.StatusInternalServerError)
617 return
618 }
619 if owner != user {
620 http.Error(w, "unauthorized", http.StatusUnauthorized)
621 return
622 }
623 data := logData{
624 Navigation: []navItem{
625 navItem{"Home", "/"},
626 navItem{appName, "/" + appName},
627 navItem{"Logs", "/" + appName + "/logs"},
628 },
629 AppName: appName,
630 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
631 }
632 if err := s.tmplts.logs.Execute(w, data); err != nil {
633 fmt.Println(err)
634 http.Error(w, err.Error(), http.StatusInternalServerError)
635 return
636 }
637}
638
gio81246f02024-07-10 12:02:15 +0400639type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400640 Ref string `json:"ref"`
641 Repository struct {
642 Name string `json:"name"`
643 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400644 After string `json:"after"`
645 Commits []struct {
646 Id string `json:"id"`
647 Message string `json:"message"`
648 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400649}
650
gio8fae3af2024-07-25 13:43:31 +0400651func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400652 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400653 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400654 var contents strings.Builder
655 io.Copy(&contents, r.Body)
656 c := contents.String()
657 fmt.Println(c)
658 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400659 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400660 return
661 }
gioa60f0de2024-07-08 10:49:48 +0400662 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400663 return
664 }
gioa60f0de2024-07-08 10:49:48 +0400665 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400666 go func() {
gio11617ac2024-07-15 16:09:04 +0400667 owner, err := s.st.GetAppOwner(req.Repository.Name)
668 if err != nil {
669 return
670 }
671 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400672 if err != nil {
673 return
674 }
gio94904702024-07-26 16:58:34 +0400675 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
676 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
677 if err != nil {
678 return
679 }
gioe2e31e12024-08-18 08:20:56 +0400680 found := false
681 commitMsg := ""
682 for _, c := range req.Commits {
683 if c.Id == req.After {
684 found = true
685 commitMsg = c.Message
686 break
gioa60f0de2024-07-08 10:49:48 +0400687 }
688 }
gioe2e31e12024-08-18 08:20:56 +0400689 if !found {
690 fmt.Printf("Error: could not find commit message")
691 return
692 }
giob4a3a192024-08-19 09:55:47 +0400693 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks)
694 if err = s.createCommit(req.Repository.Name, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400695 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400696 return
697 }
gioa60f0de2024-07-08 10:49:48 +0400698 for addr, _ := range s.workers[req.Repository.Name] {
699 go func() {
700 // TODO(gio): make port configurable
701 http.Get(fmt.Sprintf("http://%s/update", addr))
702 }()
gio0eaf2712024-04-14 13:08:46 +0400703 }
704 }()
gio0eaf2712024-04-14 13:08:46 +0400705}
706
gio81246f02024-07-10 12:02:15 +0400707type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400708 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400709 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400710}
711
gio8fae3af2024-07-25 13:43:31 +0400712func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400713 vars := mux.Vars(r)
714 appName, ok := vars["app-name"]
715 if !ok || appName == "" {
716 http.Error(w, "missing app-name", http.StatusBadRequest)
717 return
718 }
gio81246f02024-07-10 12:02:15 +0400719 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400720 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
721 http.Error(w, err.Error(), http.StatusInternalServerError)
722 return
723 }
gioa60f0de2024-07-08 10:49:48 +0400724 if _, ok := s.workers[appName]; !ok {
725 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400726 }
gioa60f0de2024-07-08 10:49:48 +0400727 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400728 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400729}
730
gio11617ac2024-07-15 16:09:04 +0400731func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
732 u := r.Context().Value(userCtx)
733 if u == nil {
734 http.Error(w, "unauthorized", http.StatusUnauthorized)
735 return
736 }
737 user, ok := u.(string)
738 if !ok {
739 http.Error(w, "could not get user", http.StatusInternalServerError)
740 return
741 }
742 network := r.FormValue("network")
743 if network == "" {
744 http.Error(w, "missing network", http.StatusBadRequest)
745 return
746 }
gio5e49bb62024-07-20 10:43:19 +0400747 subdomain := r.FormValue("subdomain")
748 if subdomain == "" {
749 http.Error(w, "missing subdomain", http.StatusBadRequest)
750 return
751 }
752 appType := r.FormValue("type")
753 if appType == "" {
754 http.Error(w, "missing type", http.StatusBadRequest)
755 return
756 }
gio11617ac2024-07-15 16:09:04 +0400757 g := installer.NewFixedLengthRandomNameGenerator(3)
758 appName, err := g.Generate()
759 if err != nil {
760 http.Error(w, err.Error(), http.StatusInternalServerError)
761 return
762 }
763 if ok, err := s.client.UserExists(user); err != nil {
764 http.Error(w, err.Error(), http.StatusInternalServerError)
765 return
766 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400767 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
768 return
gio11617ac2024-07-15 16:09:04 +0400769 }
giocafd4e62024-07-31 10:53:40 +0400770 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400771 http.Error(w, err.Error(), http.StatusInternalServerError)
772 return
773 }
774 if err := s.st.CreateApp(appName, user); err != nil {
775 http.Error(w, err.Error(), http.StatusInternalServerError)
776 return
777 }
giod8ab4f52024-07-26 16:58:34 +0400778 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400779 http.Error(w, err.Error(), http.StatusInternalServerError)
780 return
781 }
782 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
783}
784
gio81246f02024-07-10 12:02:15 +0400785type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400786 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400787 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400788 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400789 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400790}
791
gio81246f02024-07-10 12:02:15 +0400792type apiCreateAppResp struct {
793 AppName string `json:"appName"`
794 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400795}
796
gio8fae3af2024-07-25 13:43:31 +0400797func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400798 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400799 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400800 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
801 http.Error(w, err.Error(), http.StatusBadRequest)
802 return
803 }
804 g := installer.NewFixedLengthRandomNameGenerator(3)
805 appName, err := g.Generate()
806 if err != nil {
807 http.Error(w, err.Error(), http.StatusInternalServerError)
808 return
809 }
gio11617ac2024-07-15 16:09:04 +0400810 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400811 if err != nil {
gio33059762024-07-05 13:19:07 +0400812 http.Error(w, err.Error(), http.StatusInternalServerError)
813 return
814 }
gio11617ac2024-07-15 16:09:04 +0400815 if user != "" {
816 http.Error(w, "public key already registered", http.StatusBadRequest)
817 return
818 }
819 user = appName
820 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
821 http.Error(w, err.Error(), http.StatusInternalServerError)
822 return
823 }
824 password := generatePassword()
825 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
826 if err != nil {
827 http.Error(w, err.Error(), http.StatusInternalServerError)
828 return
829 }
giocafd4e62024-07-31 10:53:40 +0400830 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400831 http.Error(w, err.Error(), http.StatusInternalServerError)
832 return
833 }
834 if err := s.st.CreateApp(appName, user); err != nil {
835 http.Error(w, err.Error(), http.StatusInternalServerError)
836 return
837 }
giod8ab4f52024-07-26 16:58:34 +0400838 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400839 http.Error(w, err.Error(), http.StatusInternalServerError)
840 return
841 }
gio81246f02024-07-10 12:02:15 +0400842 resp := apiCreateAppResp{
843 AppName: appName,
844 Password: password,
845 }
gio33059762024-07-05 13:19:07 +0400846 if err := json.NewEncoder(w).Encode(resp); err != nil {
847 http.Error(w, err.Error(), http.StatusInternalServerError)
848 return
849 }
850}
851
giod8ab4f52024-07-26 16:58:34 +0400852func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400853 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400854 return true
855 }
856 for _, cfg := range s.appConfigs {
857 if strings.ToLower(cfg.Network) == network {
858 return false
859 }
860 }
861 return true
862}
863
864func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400865 s.l.Lock()
866 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400867 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400868 network = strings.ToLower(network)
869 if !s.isNetworkUseAllowed(network) {
870 return fmt.Errorf("network already used: %s", network)
871 }
gio33059762024-07-05 13:19:07 +0400872 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400873 return err
gio33059762024-07-05 13:19:07 +0400874 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400875 return nil
gioa60f0de2024-07-08 10:49:48 +0400876 }
gio5e49bb62024-07-20 10:43:19 +0400877 networks, err := s.getNetworks(user)
878 if err != nil {
879 return err
880 }
giod8ab4f52024-07-26 16:58:34 +0400881 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400882 if !ok {
883 return fmt.Errorf("network not found: %s\n", network)
884 }
gio33059762024-07-05 13:19:07 +0400885 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400886 return err
gio33059762024-07-05 13:19:07 +0400887 }
888 appRepo, err := s.client.GetRepo(appName)
889 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400890 return err
gio33059762024-07-05 13:19:07 +0400891 }
giob4a3a192024-08-19 09:55:47 +0400892 commit, err := s.initRepo(appRepo, appType, n, subdomain)
893 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400894 return err
gio33059762024-07-05 13:19:07 +0400895 }
896 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400897 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
898 if err != nil {
899 return err
900 }
901 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400902 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400903 return err
gio33059762024-07-05 13:19:07 +0400904 }
905 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
906 suffix, err := suffixGen.Generate()
907 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400908 return err
gio33059762024-07-05 13:19:07 +0400909 }
gio94904702024-07-26 16:58:34 +0400910 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400911 s.appConfigs[appName] = appConfig{namespace, network}
giob4a3a192024-08-19 09:55:47 +0400912 resources, err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks)
913 if err != nil {
914 return err
915 }
916 if err = s.createCommit(appName, commit, initCommitMsg, err, resources); err != nil {
917 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +0400918 return err
gio33059762024-07-05 13:19:07 +0400919 }
giod8ab4f52024-07-26 16:58:34 +0400920 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400921 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400922 return err
gio33059762024-07-05 13:19:07 +0400923 }
924 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400925 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400926 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400927 return err
gio33059762024-07-05 13:19:07 +0400928 }
giob4a3a192024-08-19 09:55:47 +0400929 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +0400930 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400931 if err != nil {
932 return "", err
933 }
934 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400935 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400936 return "", err
937 }
938 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400939 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400940 appName,
941 "/"+appName,
942 namespace,
943 map[string]any{
944 "repoAddr": s.client.GetRepoAddress(appName),
945 "repoHost": strings.Split(s.client.Address(), ":")[0],
946 "gitRepoPublicKey": s.gitRepoPublicKey,
947 },
948 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400949 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400950 installer.WithNoPublish(),
951 installer.WithNoLock(),
952 ); err != nil {
953 return "", err
954 }
955 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +0400956 })
957 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400958 return err
gio33059762024-07-05 13:19:07 +0400959 }
960 cfg, err := m.FindInstance(appName)
961 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400962 return err
gio33059762024-07-05 13:19:07 +0400963 }
964 fluxKeys, ok := cfg.Input["fluxKeys"]
965 if !ok {
gio11617ac2024-07-15 16:09:04 +0400966 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400967 }
968 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
969 if !ok {
gio11617ac2024-07-15 16:09:04 +0400970 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400971 }
972 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400973 return err
gio33059762024-07-05 13:19:07 +0400974 } else if ok {
975 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400976 return err
gio33059762024-07-05 13:19:07 +0400977 }
978 } else {
979 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400980 return err
gio33059762024-07-05 13:19:07 +0400981 }
982 }
983 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400984 return err
gio33059762024-07-05 13:19:07 +0400985 }
986 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 +0400987 return err
gio33059762024-07-05 13:19:07 +0400988 }
gio81246f02024-07-10 12:02:15 +0400989 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400990 return err
gio33059762024-07-05 13:19:07 +0400991 }
gio2ccb6e32024-08-15 12:01:33 +0400992 if !s.external {
993 go func() {
994 users, err := s.client.GetAllUsers()
995 if err != nil {
996 fmt.Println(err)
997 return
998 }
999 for _, user := range users {
1000 // TODO(gio): fluxcd should have only read access
1001 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1002 fmt.Println(err)
1003 }
1004 }
1005 }()
1006 }
gio11617ac2024-07-15 16:09:04 +04001007 return nil
gio33059762024-07-05 13:19:07 +04001008}
1009
gio81246f02024-07-10 12:02:15 +04001010type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +04001011 Public string `json:"public"`
1012}
1013
gio8fae3af2024-07-25 13:43:31 +04001014func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001015 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001016 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1017 http.Error(w, err.Error(), http.StatusBadRequest)
1018 return
1019 }
1020 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
1021 http.Error(w, err.Error(), http.StatusInternalServerError)
1022 return
1023 }
1024}
1025
gio94904702024-07-26 16:58:34 +04001026type dodoAppRendered struct {
1027 App struct {
1028 Ingress struct {
1029 Network string `json:"network"`
1030 Subdomain string `json:"subdomain"`
1031 } `json:"ingress"`
1032 } `json:"app"`
1033 Input struct {
1034 AppId string `json:"appId"`
1035 } `json:"input"`
1036}
1037
giob4a3a192024-08-19 09:55:47 +04001038func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) (installer.ReleaseResources, error) {
gio1bf00802024-08-17 12:31:41 +04001039 fmt.Println("111")
gio33059762024-07-05 13:19:07 +04001040 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +04001041 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001042 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001043 }
gio1bf00802024-08-17 12:31:41 +04001044 fmt.Println("111")
giof8843412024-05-22 16:38:05 +04001045 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +04001046 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001047 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001048 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001049 }
gio1bf00802024-08-17 12:31:41 +04001050 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +04001051 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +04001052 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001053 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001054 }
gio1bf00802024-08-17 12:31:41 +04001055 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +04001056 app, err := installer.NewDodoApp(appCfg)
1057 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001058 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001059 }
gio1bf00802024-08-17 12:31:41 +04001060 fmt.Println("111")
giof8843412024-05-22 16:38:05 +04001061 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001062 var ret installer.ReleaseResources
1063 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1064 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001065 app,
1066 "app",
1067 "/.dodo/app",
1068 namespace,
1069 map[string]any{
1070 "repoAddr": repo.FullAddress(),
1071 "managerAddr": fmt.Sprintf("http://%s", s.self),
1072 "appId": name,
1073 "sshPrivateKey": s.sshKey,
1074 },
1075 installer.WithNoPull(),
1076 installer.WithNoPublish(),
1077 installer.WithConfig(&s.env),
1078 installer.WithNetworks(networks),
1079 installer.WithLocalChartGenerator(lg),
1080 installer.WithNoLock(),
1081 )
gio1bf00802024-08-17 12:31:41 +04001082 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001083 if err != nil {
1084 return "", err
1085 }
gio1bf00802024-08-17 12:31:41 +04001086 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001087 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001088 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001089 return "", nil
1090 }
gio1bf00802024-08-17 12:31:41 +04001091 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001092 if _, err := m.Install(
1093 appStatus,
1094 "status",
1095 "/.dodo/status",
1096 s.namespace,
1097 map[string]any{
1098 "appName": rendered.Input.AppId,
1099 "network": rendered.App.Ingress.Network,
1100 "appSubdomain": rendered.App.Ingress.Subdomain,
1101 },
1102 installer.WithNoPull(),
1103 installer.WithNoPublish(),
1104 installer.WithConfig(&s.env),
1105 installer.WithNetworks(networks),
1106 installer.WithLocalChartGenerator(lg),
1107 installer.WithNoLock(),
1108 ); err != nil {
1109 return "", err
1110 }
gio1bf00802024-08-17 12:31:41 +04001111 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001112 return "install app", nil
1113 },
1114 soft.WithCommitToBranch("dodo"),
1115 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001116 ); err != nil {
1117 return installer.ReleaseResources{}, err
1118 }
1119 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001120}
gio33059762024-07-05 13:19:07 +04001121
giob4a3a192024-08-19 09:55:47 +04001122func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) (string, error) {
giob54db242024-07-30 18:49:33 +04001123 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001124 appTmpl, err := s.appTmpls.Find(appType)
1125 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001126 return "", err
gio33059762024-07-05 13:19:07 +04001127 }
gio33059762024-07-05 13:19:07 +04001128 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +04001129 if err := appTmpl.Render(network, subdomain, repo); err != nil {
1130 return "", err
gio33059762024-07-05 13:19:07 +04001131 }
giob4a3a192024-08-19 09:55:47 +04001132 return initCommitMsg, nil
gio33059762024-07-05 13:19:07 +04001133 })
1134}
gio81246f02024-07-10 12:02:15 +04001135
1136func generatePassword() string {
1137 return "foo"
1138}
giocb34ad22024-07-11 08:01:13 +04001139
gio11617ac2024-07-15 16:09:04 +04001140func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001141 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001142 resp, err := http.Get(addr)
1143 if err != nil {
1144 return nil, err
1145 }
gio23bdc1b2024-07-11 16:07:47 +04001146 networks := []installer.Network{}
1147 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001148 return nil, err
1149 }
gio11617ac2024-07-15 16:09:04 +04001150 return s.nf.Filter(user, networks)
1151}
1152
gio8fae3af2024-07-25 13:43:31 +04001153type publicNetworkData struct {
1154 Name string `json:"name"`
1155 Domain string `json:"domain"`
1156}
1157
1158type publicData struct {
1159 Networks []publicNetworkData `json:"networks"`
1160 Types []string `json:"types"`
1161}
1162
1163func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001164 w.Header().Set("Access-Control-Allow-Origin", "*")
1165 s.l.Lock()
1166 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001167 networks, err := s.getNetworks("")
1168 if err != nil {
1169 http.Error(w, err.Error(), http.StatusInternalServerError)
1170 return
1171 }
1172 var ret publicData
1173 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001174 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1175 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1176 }
gio8fae3af2024-07-25 13:43:31 +04001177 }
1178 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001179 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001180 }
gio8fae3af2024-07-25 13:43:31 +04001181 if err := json.NewEncoder(w).Encode(ret); err != nil {
1182 http.Error(w, err.Error(), http.StatusInternalServerError)
1183 return
1184 }
1185}
1186
giob4a3a192024-08-19 09:55:47 +04001187func (s *DodoAppServer) createCommit(name, hash, message string, err error, resources installer.ReleaseResources) error {
1188 if err != nil {
1189 fmt.Printf("Error: %s\n", err.Error())
1190 if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
1191 fmt.Printf("Error: %s\n", err.Error())
1192 return err
1193 }
1194 return err
1195 }
1196 var resB bytes.Buffer
1197 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
1198 if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
1199 fmt.Printf("Error: %s\n", err.Error())
1200 return err
1201 }
1202 return err
1203 }
1204 if err := s.st.CreateCommit(name, hash, message, "OK", "", resB.Bytes()); err != nil {
1205 fmt.Printf("Error: %s\n", err.Error())
1206 return err
1207 }
1208 return nil
1209}
1210
gio11617ac2024-07-15 16:09:04 +04001211func pickNetwork(networks []installer.Network, network string) []installer.Network {
1212 for _, n := range networks {
1213 if n.Name == network {
1214 return []installer.Network{n}
1215 }
1216 }
1217 return []installer.Network{}
1218}
1219
1220type NetworkFilter interface {
1221 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1222}
1223
1224type noNetworkFilter struct{}
1225
1226func NewNoNetworkFilter() NetworkFilter {
1227 return noNetworkFilter{}
1228}
1229
gio8fae3af2024-07-25 13:43:31 +04001230func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001231 return networks, nil
1232}
1233
1234type filterByOwner struct {
1235 st Store
1236}
1237
1238func NewNetworkFilterByOwner(st Store) NetworkFilter {
1239 return &filterByOwner{st}
1240}
1241
1242func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001243 if user == "" {
1244 return networks, nil
1245 }
gio11617ac2024-07-15 16:09:04 +04001246 network, err := f.st.GetUserNetwork(user)
1247 if err != nil {
1248 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001249 }
1250 ret := []installer.Network{}
1251 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001252 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001253 ret = append(ret, n)
1254 }
1255 }
giocb34ad22024-07-11 08:01:13 +04001256 return ret, nil
1257}
gio11617ac2024-07-15 16:09:04 +04001258
1259type allowListFilter struct {
1260 allowed []string
1261}
1262
1263func NewAllowListFilter(allowed []string) NetworkFilter {
1264 return &allowListFilter{allowed}
1265}
1266
gio8fae3af2024-07-25 13:43:31 +04001267func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001268 ret := []installer.Network{}
1269 for _, n := range networks {
1270 if slices.Contains(f.allowed, n.Name) {
1271 ret = append(ret, n)
1272 }
1273 }
1274 return ret, nil
1275}
1276
1277type combinedNetworkFilter struct {
1278 filters []NetworkFilter
1279}
1280
1281func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1282 return &combinedNetworkFilter{filters}
1283}
1284
gio8fae3af2024-07-25 13:43:31 +04001285func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001286 ret := networks
1287 var err error
1288 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001289 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001290 if err != nil {
1291 return nil, err
1292 }
1293 }
1294 return ret, nil
1295}
giocafd4e62024-07-31 10:53:40 +04001296
1297type user struct {
1298 Username string `json:"username"`
1299 Email string `json:"email"`
1300 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1301}
1302
1303func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1304 go s.syncUsers()
1305}
1306
1307func (s *DodoAppServer) syncUsers() {
1308 if s.external {
1309 panic("MUST NOT REACH!")
1310 }
1311 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1312 if err != nil {
1313 return
1314 }
1315 users := []user{}
1316 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1317 fmt.Println(err)
1318 return
1319 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001320 validUsernames := make(map[string]user)
1321 for _, u := range users {
1322 validUsernames[u.Username] = u
1323 }
1324 allClientUsers, err := s.client.GetAllUsers()
1325 if err != nil {
1326 fmt.Println(err)
1327 return
1328 }
1329 keyToUser := make(map[string]string)
1330 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001331 if clientUser == "admin" || clientUser == "fluxcd" {
1332 continue
1333 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001334 userData, ok := validUsernames[clientUser]
1335 if !ok {
1336 if err := s.client.RemoveUser(clientUser); err != nil {
1337 fmt.Println(err)
1338 return
1339 }
1340 } else {
1341 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1342 if err != nil {
1343 fmt.Println(err)
1344 return
1345 }
1346 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001347 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001348 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001349 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001350 })
1351 if !keyOk {
1352 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1353 fmt.Println(err)
1354 }
1355 } else {
1356 keyToUser[cleanKey] = clientUser
1357 }
1358 }
1359 }
1360 }
giocafd4e62024-07-31 10:53:40 +04001361 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001362 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1363 fmt.Println(err)
1364 return
1365 }
giocafd4e62024-07-31 10:53:40 +04001366 if len(u.SSHPublicKeys) == 0 {
1367 continue
1368 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001369 ok, err := s.client.UserExists(u.Username)
1370 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001371 fmt.Println(err)
1372 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001373 }
1374 if !ok {
1375 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1376 fmt.Println(err)
1377 return
1378 }
1379 } else {
1380 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001381 cleanKey := soft.CleanKey(key)
1382 if user, ok := keyToUser[cleanKey]; ok {
1383 if u.Username != user {
1384 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1385 }
1386 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001387 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001388 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001389 fmt.Println(err)
1390 return
giocafd4e62024-07-31 10:53:40 +04001391 }
1392 }
1393 }
1394 }
1395 repos, err := s.client.GetAllRepos()
1396 if err != nil {
1397 return
1398 }
1399 for _, r := range repos {
1400 if r == ConfigRepoName {
1401 continue
1402 }
1403 for _, u := range users {
1404 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1405 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001406 continue
giocafd4e62024-07-31 10:53:40 +04001407 }
1408 }
1409 }
1410}
giob4a3a192024-08-19 09:55:47 +04001411
1412func extractResourceData(resources []installer.Resource) (resourceData, error) {
1413 var ret resourceData
1414 for _, r := range resources {
1415 t, ok := r.Annotations["dodo.cloud/resource-type"]
1416 if !ok {
1417 continue
1418 }
1419 switch t {
1420 case "volume":
1421 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1422 if !ok {
1423 return resourceData{}, fmt.Errorf("no name")
1424 }
1425 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1426 if !ok {
1427 return resourceData{}, fmt.Errorf("no size")
1428 }
1429 ret.Volume = append(ret.Volume, volume{name, size})
1430 case "postgresql":
1431 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1432 if !ok {
1433 return resourceData{}, fmt.Errorf("no name")
1434 }
1435 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1436 if !ok {
1437 return resourceData{}, fmt.Errorf("no version")
1438 }
1439 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1440 if !ok {
1441 return resourceData{}, fmt.Errorf("no volume")
1442 }
1443 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1444 case "ingress":
1445 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1446 if !ok {
1447 return resourceData{}, fmt.Errorf("no host")
1448 }
1449 ret.Ingress = append(ret.Ingress, ingress{host})
1450 default:
1451 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1452 }
1453 }
1454 return ret, nil
1455}