blob: 43a6bd71041f69f3bfd83edb6db31392146671ba [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
gio23bdc1b2024-07-11 16:07:47 +040052}
53
54func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040055 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040056 if err != nil {
57 return dodoAppTmplts{}, err
58 }
gio5e49bb62024-07-20 10:43:19 +040059 parse := func(path string) (*template.Template, error) {
60 if b, err := base.Clone(); err != nil {
61 return nil, err
62 } else {
63 return b.ParseFS(fs, path)
64 }
65 }
66 index, err := parse("dodo-app-tmpl/index.html")
67 if err != nil {
68 return dodoAppTmplts{}, err
69 }
70 appStatus, err := parse("dodo-app-tmpl/app_status.html")
71 if err != nil {
72 return dodoAppTmplts{}, err
73 }
giob4a3a192024-08-19 09:55:47 +040074 commitStatus, err := parse("dodo-app-tmpl/commit_status.html")
75 if err != nil {
76 return dodoAppTmplts{}, err
77 }
78 return dodoAppTmplts{index, appStatus, commitStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040079}
80
gio0eaf2712024-04-14 13:08:46 +040081type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040082 l sync.Locker
83 st Store
gio11617ac2024-07-15 16:09:04 +040084 nf NetworkFilter
85 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040086 port int
87 apiPort int
88 self string
gio11617ac2024-07-15 16:09:04 +040089 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040090 sshKey string
91 gitRepoPublicKey string
92 client soft.Client
93 namespace string
94 envAppManagerAddr string
95 env installer.EnvConfig
96 nsc installer.NamespaceCreator
97 jc installer.JobCreator
98 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040099 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +0400100 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +0400101 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400102 external bool
103 fetchUsersAddr string
giod8ab4f52024-07-26 16:58:34 +0400104}
105
106type appConfig struct {
107 Namespace string `json:"namespace"`
108 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400109}
110
gio33059762024-07-05 13:19:07 +0400111// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400112func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400113 st Store,
gio11617ac2024-07-15 16:09:04 +0400114 nf NetworkFilter,
115 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400116 port int,
gioa60f0de2024-07-08 10:49:48 +0400117 apiPort int,
gio33059762024-07-05 13:19:07 +0400118 self string,
gio11617ac2024-07-15 16:09:04 +0400119 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400120 sshKey string,
gio33059762024-07-05 13:19:07 +0400121 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400122 client soft.Client,
123 namespace string,
giocb34ad22024-07-11 08:01:13 +0400124 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400125 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400126 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400127 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400128 external bool,
129 fetchUsersAddr string,
gio9d66f322024-07-06 13:45:10 +0400130) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400131 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
132 if err != nil {
133 return nil, err
134 }
gio5e49bb62024-07-20 10:43:19 +0400135 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
136 if err != nil {
137 return nil, err
138 }
139 appTmpls, err := NewAppTmplStoreFS(apps)
140 if err != nil {
141 return nil, err
142 }
gio9d66f322024-07-06 13:45:10 +0400143 s := &DodoAppServer{
144 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400145 st,
gio11617ac2024-07-15 16:09:04 +0400146 nf,
147 ug,
gio0eaf2712024-04-14 13:08:46 +0400148 port,
gioa60f0de2024-07-08 10:49:48 +0400149 apiPort,
gio33059762024-07-05 13:19:07 +0400150 self,
gio11617ac2024-07-15 16:09:04 +0400151 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400152 sshKey,
gio33059762024-07-05 13:19:07 +0400153 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400154 client,
155 namespace,
giocb34ad22024-07-11 08:01:13 +0400156 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400157 env,
gio33059762024-07-05 13:19:07 +0400158 nsc,
giof8843412024-05-22 16:38:05 +0400159 jc,
gio266c04f2024-07-03 14:18:45 +0400160 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400161 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400162 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400163 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400164 external,
165 fetchUsersAddr,
gio0eaf2712024-04-14 13:08:46 +0400166 }
gioa60f0de2024-07-08 10:49:48 +0400167 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400168 if err != nil {
169 return nil, err
170 }
giod8ab4f52024-07-26 16:58:34 +0400171 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400172 if err == nil {
173 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400174 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400175 return nil, err
176 }
177 } else if !errors.Is(err, fs.ErrNotExist) {
178 return nil, err
179 }
180 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400181}
182
183func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400184 e := make(chan error)
185 go func() {
186 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400187 r.Use(s.mwAuth)
gio1bf00802024-08-17 12:31:41 +0400188 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400189 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400190 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
191 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400192 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
193 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
giob4a3a192024-08-19 09:55:47 +0400194 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio81246f02024-07-10 12:02:15 +0400195 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
196 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400197 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400198 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
199 }()
200 go func() {
201 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400202 r.HandleFunc("/update", s.handleAPIUpdate)
203 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
204 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
giocafd4e62024-07-31 10:53:40 +0400205 if !s.external {
206 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
207 }
gioa60f0de2024-07-08 10:49:48 +0400208 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
209 }()
giocafd4e62024-07-31 10:53:40 +0400210 if !s.external {
211 go func() {
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400212 rand.Seed(uint64(time.Now().UnixNano()))
giocafd4e62024-07-31 10:53:40 +0400213 s.syncUsers()
214 // TODO(dtabidze): every sync delay should be randomized to avoid all client
215 // applications hitting memberships service at the same time.
216 // For every next sync new delay should be randomly generated from scratch.
217 // We can choose random delay from 1 to 2 minutes.
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400218 // for range time.Tick(1 * time.Minute) {
219 // s.syncUsers()
220 // }
221 for {
222 delay := time.Duration(rand.Intn(60)+60) * time.Second
223 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400224 s.syncUsers()
225 }
226 }()
227 }
gioa60f0de2024-07-08 10:49:48 +0400228 return <-e
229}
230
gio11617ac2024-07-15 16:09:04 +0400231type UserGetter interface {
232 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400233 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400234}
235
236type externalUserGetter struct {
237 sc *securecookie.SecureCookie
238}
239
240func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400241 return &externalUserGetter{securecookie.New(
242 securecookie.GenerateRandomKey(64),
243 securecookie.GenerateRandomKey(32),
244 )}
gio11617ac2024-07-15 16:09:04 +0400245}
246
247func (ug *externalUserGetter) Get(r *http.Request) string {
248 cookie, err := r.Cookie(sessionCookie)
249 if err != nil {
250 return ""
251 }
252 var user string
253 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
254 return ""
255 }
256 return user
257}
258
gio8fae3af2024-07-25 13:43:31 +0400259func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
260 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
261 cookie := &http.Cookie{
262 Name: sessionCookie,
263 Value: encoded,
264 Path: "/",
265 Secure: true,
266 HttpOnly: true,
267 }
268 http.SetCookie(w, cookie)
269 return nil
270 } else {
271 return err
272 }
273}
274
gio11617ac2024-07-15 16:09:04 +0400275type internalUserGetter struct{}
276
277func NewInternalUserGetter() UserGetter {
278 return internalUserGetter{}
279}
280
281func (ug internalUserGetter) Get(r *http.Request) string {
282 return r.Header.Get("X-User")
283}
284
gio8fae3af2024-07-25 13:43:31 +0400285func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
286 return nil
287}
288
gio81246f02024-07-10 12:02:15 +0400289func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
290 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400291 if strings.HasSuffix(r.URL.Path, loginPath) ||
292 strings.HasPrefix(r.URL.Path, logoutPath) ||
293 strings.HasPrefix(r.URL.Path, staticPath) ||
294 strings.HasPrefix(r.URL.Path, apiPublicData) ||
295 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400296 next.ServeHTTP(w, r)
297 return
298 }
gio11617ac2024-07-15 16:09:04 +0400299 user := s.ug.Get(r)
300 if user == "" {
gio81246f02024-07-10 12:02:15 +0400301 vars := mux.Vars(r)
302 appName, ok := vars["app-name"]
303 if !ok || appName == "" {
304 http.Error(w, "missing app-name", http.StatusBadRequest)
305 return
306 }
307 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
308 return
309 }
gio81246f02024-07-10 12:02:15 +0400310 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
311 })
312}
313
314func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400315 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400316 http.SetCookie(w, &http.Cookie{
317 Name: sessionCookie,
318 Value: "",
319 Path: "/",
320 HttpOnly: true,
321 Secure: true,
322 })
323 http.Redirect(w, r, "/", http.StatusSeeOther)
324}
325
326func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
327 vars := mux.Vars(r)
328 appName, ok := vars["app-name"]
329 if !ok || appName == "" {
330 http.Error(w, "missing app-name", http.StatusBadRequest)
331 return
332 }
333 fmt.Fprint(w, `
334<!DOCTYPE html>
335<html lang='en'>
336 <head>
337 <title>dodo: app - login</title>
338 <meta charset='utf-8'>
339 </head>
340 <body>
341 <form action="" method="POST">
342 <input type="password" placeholder="Password" name="password" required />
343 <button type="submit">Login</button>
344 </form>
345 </body>
346</html>
347`)
348}
349
350func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
351 vars := mux.Vars(r)
352 appName, ok := vars["app-name"]
353 if !ok || appName == "" {
354 http.Error(w, "missing app-name", http.StatusBadRequest)
355 return
356 }
357 password := r.FormValue("password")
358 if password == "" {
359 http.Error(w, "missing password", http.StatusBadRequest)
360 return
361 }
362 user, err := s.st.GetAppOwner(appName)
363 if err != nil {
364 http.Error(w, err.Error(), http.StatusInternalServerError)
365 return
366 }
367 hashed, err := s.st.GetUserPassword(user)
368 if err != nil {
369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
371 }
372 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
373 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
374 return
375 }
gio8fae3af2024-07-25 13:43:31 +0400376 if err := s.ug.Encode(w, user); err != nil {
377 http.Error(w, err.Error(), http.StatusInternalServerError)
378 return
gio81246f02024-07-10 12:02:15 +0400379 }
380 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
381}
382
giob4a3a192024-08-19 09:55:47 +0400383type navItem struct {
384 Name string
385 Address string
386}
387
gio23bdc1b2024-07-11 16:07:47 +0400388type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400389 Navigation []navItem
390 Apps []string
391 Networks []installer.Network
392 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400393}
394
gioa60f0de2024-07-08 10:49:48 +0400395func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400396 user := r.Context().Value(userCtx)
397 if user == nil {
398 http.Error(w, "unauthorized", http.StatusUnauthorized)
399 return
400 }
401 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400402 if err != nil {
403 http.Error(w, err.Error(), http.StatusInternalServerError)
404 return
405 }
gio11617ac2024-07-15 16:09:04 +0400406 networks, err := s.getNetworks(user.(string))
407 if err != nil {
408 http.Error(w, err.Error(), http.StatusInternalServerError)
409 return
410 }
giob54db242024-07-30 18:49:33 +0400411 var types []string
412 for _, t := range s.appTmpls.Types() {
413 types = append(types, strings.Replace(t, "-", ":", 1))
414 }
giob4a3a192024-08-19 09:55:47 +0400415 n := []navItem{navItem{"Home", "/"}}
416 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400417 if err := s.tmplts.index.Execute(w, data); err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
gioa60f0de2024-07-08 10:49:48 +0400420 }
421}
422
gio5e49bb62024-07-20 10:43:19 +0400423type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400424 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400425 Name string
426 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400427 Commits []CommitMeta
gio5e49bb62024-07-20 10:43:19 +0400428}
429
gioa60f0de2024-07-08 10:49:48 +0400430func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
431 vars := mux.Vars(r)
432 appName, ok := vars["app-name"]
433 if !ok || appName == "" {
434 http.Error(w, "missing app-name", http.StatusBadRequest)
435 return
436 }
gio94904702024-07-26 16:58:34 +0400437 u := r.Context().Value(userCtx)
438 if u == nil {
439 http.Error(w, "unauthorized", http.StatusUnauthorized)
440 return
441 }
442 user, ok := u.(string)
443 if !ok {
444 http.Error(w, "could not get user", http.StatusInternalServerError)
445 return
446 }
447 owner, err := s.st.GetAppOwner(appName)
448 if err != nil {
449 http.Error(w, err.Error(), http.StatusInternalServerError)
450 return
451 }
452 if owner != user {
453 http.Error(w, "unauthorized", http.StatusUnauthorized)
454 return
455 }
gioa60f0de2024-07-08 10:49:48 +0400456 commits, err := s.st.GetCommitHistory(appName)
457 if err != nil {
458 http.Error(w, err.Error(), http.StatusInternalServerError)
459 return
460 }
gio5e49bb62024-07-20 10:43:19 +0400461 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400462 Navigation: []navItem{
463 navItem{"Home", "/"},
464 navItem{appName, "/" + appName},
465 },
gio5e49bb62024-07-20 10:43:19 +0400466 Name: appName,
467 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
468 Commits: commits,
469 }
470 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
471 http.Error(w, err.Error(), http.StatusInternalServerError)
472 return
gioa60f0de2024-07-08 10:49:48 +0400473 }
gio0eaf2712024-04-14 13:08:46 +0400474}
475
giob4a3a192024-08-19 09:55:47 +0400476type volume struct {
477 Name string
478 Size string
479}
480
481type postgresql struct {
482 Name string
483 Version string
484 Volume string
485}
486
487type ingress struct {
488 Host string
489}
490
491type resourceData struct {
492 Volume []volume
493 PostgreSQL []postgresql
494 Ingress []ingress
495}
496
497type commitStatusData struct {
498 Navigation []navItem
499 AppName string
500 Commit Commit
501 Resources resourceData
502}
503
504func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
505 vars := mux.Vars(r)
506 appName, ok := vars["app-name"]
507 if !ok || appName == "" {
508 http.Error(w, "missing app-name", http.StatusBadRequest)
509 return
510 }
511 hash, ok := vars["hash"]
512 if !ok || appName == "" {
513 http.Error(w, "missing app-name", http.StatusBadRequest)
514 return
515 }
516 u := r.Context().Value(userCtx)
517 if u == nil {
518 http.Error(w, "unauthorized", http.StatusUnauthorized)
519 return
520 }
521 user, ok := u.(string)
522 if !ok {
523 http.Error(w, "could not get user", http.StatusInternalServerError)
524 return
525 }
526 owner, err := s.st.GetAppOwner(appName)
527 if err != nil {
528 http.Error(w, err.Error(), http.StatusInternalServerError)
529 return
530 }
531 if owner != user {
532 http.Error(w, "unauthorized", http.StatusUnauthorized)
533 return
534 }
535 commit, err := s.st.GetCommit(hash)
536 if err != nil {
537 // TODO(gio): not-found ?
538 http.Error(w, err.Error(), http.StatusInternalServerError)
539 return
540 }
541 var res strings.Builder
542 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
543 http.Error(w, err.Error(), http.StatusInternalServerError)
544 return
545 }
546 resData, err := extractResourceData(commit.Resources.Helm)
547 if err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
550 }
551 data := commitStatusData{
552 Navigation: []navItem{
553 navItem{"Home", "/"},
554 navItem{appName, "/" + appName},
555 navItem{hash, "/" + appName + "/" + hash},
556 },
557 AppName: appName,
558 Commit: commit,
559 Resources: resData,
560 }
561 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 }
565}
566
gio81246f02024-07-10 12:02:15 +0400567type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400568 Ref string `json:"ref"`
569 Repository struct {
570 Name string `json:"name"`
571 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400572 After string `json:"after"`
573 Commits []struct {
574 Id string `json:"id"`
575 Message string `json:"message"`
576 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400577}
578
gio8fae3af2024-07-25 13:43:31 +0400579func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400580 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400581 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400582 var contents strings.Builder
583 io.Copy(&contents, r.Body)
584 c := contents.String()
585 fmt.Println(c)
586 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400587 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400588 return
589 }
gioa60f0de2024-07-08 10:49:48 +0400590 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400591 return
592 }
gioa60f0de2024-07-08 10:49:48 +0400593 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400594 go func() {
gio11617ac2024-07-15 16:09:04 +0400595 owner, err := s.st.GetAppOwner(req.Repository.Name)
596 if err != nil {
597 return
598 }
599 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400600 if err != nil {
601 return
602 }
gio94904702024-07-26 16:58:34 +0400603 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
604 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
605 if err != nil {
606 return
607 }
gioe2e31e12024-08-18 08:20:56 +0400608 found := false
609 commitMsg := ""
610 for _, c := range req.Commits {
611 if c.Id == req.After {
612 found = true
613 commitMsg = c.Message
614 break
gioa60f0de2024-07-08 10:49:48 +0400615 }
616 }
gioe2e31e12024-08-18 08:20:56 +0400617 if !found {
618 fmt.Printf("Error: could not find commit message")
619 return
620 }
giob4a3a192024-08-19 09:55:47 +0400621 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks)
622 if err = s.createCommit(req.Repository.Name, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400623 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400624 return
625 }
gioa60f0de2024-07-08 10:49:48 +0400626 for addr, _ := range s.workers[req.Repository.Name] {
627 go func() {
628 // TODO(gio): make port configurable
629 http.Get(fmt.Sprintf("http://%s/update", addr))
630 }()
gio0eaf2712024-04-14 13:08:46 +0400631 }
632 }()
gio0eaf2712024-04-14 13:08:46 +0400633}
634
gio81246f02024-07-10 12:02:15 +0400635type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400636 Address string `json:"address"`
637}
638
gio8fae3af2024-07-25 13:43:31 +0400639func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400640 vars := mux.Vars(r)
641 appName, ok := vars["app-name"]
642 if !ok || appName == "" {
643 http.Error(w, "missing app-name", http.StatusBadRequest)
644 return
645 }
gio81246f02024-07-10 12:02:15 +0400646 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400647 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
gioa60f0de2024-07-08 10:49:48 +0400651 if _, ok := s.workers[appName]; !ok {
652 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400653 }
gioa60f0de2024-07-08 10:49:48 +0400654 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400655}
656
gio11617ac2024-07-15 16:09:04 +0400657func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
658 u := r.Context().Value(userCtx)
659 if u == nil {
660 http.Error(w, "unauthorized", http.StatusUnauthorized)
661 return
662 }
663 user, ok := u.(string)
664 if !ok {
665 http.Error(w, "could not get user", http.StatusInternalServerError)
666 return
667 }
668 network := r.FormValue("network")
669 if network == "" {
670 http.Error(w, "missing network", http.StatusBadRequest)
671 return
672 }
gio5e49bb62024-07-20 10:43:19 +0400673 subdomain := r.FormValue("subdomain")
674 if subdomain == "" {
675 http.Error(w, "missing subdomain", http.StatusBadRequest)
676 return
677 }
678 appType := r.FormValue("type")
679 if appType == "" {
680 http.Error(w, "missing type", http.StatusBadRequest)
681 return
682 }
gio11617ac2024-07-15 16:09:04 +0400683 g := installer.NewFixedLengthRandomNameGenerator(3)
684 appName, err := g.Generate()
685 if err != nil {
686 http.Error(w, err.Error(), http.StatusInternalServerError)
687 return
688 }
689 if ok, err := s.client.UserExists(user); err != nil {
690 http.Error(w, err.Error(), http.StatusInternalServerError)
691 return
692 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400693 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
694 return
gio11617ac2024-07-15 16:09:04 +0400695 }
giocafd4e62024-07-31 10:53:40 +0400696 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400697 http.Error(w, err.Error(), http.StatusInternalServerError)
698 return
699 }
700 if err := s.st.CreateApp(appName, user); err != nil {
701 http.Error(w, err.Error(), http.StatusInternalServerError)
702 return
703 }
giod8ab4f52024-07-26 16:58:34 +0400704 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400705 http.Error(w, err.Error(), http.StatusInternalServerError)
706 return
707 }
708 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
709}
710
gio81246f02024-07-10 12:02:15 +0400711type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400712 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400713 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400714 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400715 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400716}
717
gio81246f02024-07-10 12:02:15 +0400718type apiCreateAppResp struct {
719 AppName string `json:"appName"`
720 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400721}
722
gio8fae3af2024-07-25 13:43:31 +0400723func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400724 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400725 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400726 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
727 http.Error(w, err.Error(), http.StatusBadRequest)
728 return
729 }
730 g := installer.NewFixedLengthRandomNameGenerator(3)
731 appName, err := g.Generate()
732 if err != nil {
733 http.Error(w, err.Error(), http.StatusInternalServerError)
734 return
735 }
gio11617ac2024-07-15 16:09:04 +0400736 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400737 if err != nil {
gio33059762024-07-05 13:19:07 +0400738 http.Error(w, err.Error(), http.StatusInternalServerError)
739 return
740 }
gio11617ac2024-07-15 16:09:04 +0400741 if user != "" {
742 http.Error(w, "public key already registered", http.StatusBadRequest)
743 return
744 }
745 user = appName
746 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
747 http.Error(w, err.Error(), http.StatusInternalServerError)
748 return
749 }
750 password := generatePassword()
751 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
752 if err != nil {
753 http.Error(w, err.Error(), http.StatusInternalServerError)
754 return
755 }
giocafd4e62024-07-31 10:53:40 +0400756 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400757 http.Error(w, err.Error(), http.StatusInternalServerError)
758 return
759 }
760 if err := s.st.CreateApp(appName, user); err != nil {
761 http.Error(w, err.Error(), http.StatusInternalServerError)
762 return
763 }
giod8ab4f52024-07-26 16:58:34 +0400764 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400765 http.Error(w, err.Error(), http.StatusInternalServerError)
766 return
767 }
gio81246f02024-07-10 12:02:15 +0400768 resp := apiCreateAppResp{
769 AppName: appName,
770 Password: password,
771 }
gio33059762024-07-05 13:19:07 +0400772 if err := json.NewEncoder(w).Encode(resp); err != nil {
773 http.Error(w, err.Error(), http.StatusInternalServerError)
774 return
775 }
776}
777
giod8ab4f52024-07-26 16:58:34 +0400778func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400779 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400780 return true
781 }
782 for _, cfg := range s.appConfigs {
783 if strings.ToLower(cfg.Network) == network {
784 return false
785 }
786 }
787 return true
788}
789
790func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400791 s.l.Lock()
792 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400793 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400794 network = strings.ToLower(network)
795 if !s.isNetworkUseAllowed(network) {
796 return fmt.Errorf("network already used: %s", network)
797 }
gio33059762024-07-05 13:19:07 +0400798 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400799 return err
gio33059762024-07-05 13:19:07 +0400800 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400801 return nil
gioa60f0de2024-07-08 10:49:48 +0400802 }
gio5e49bb62024-07-20 10:43:19 +0400803 networks, err := s.getNetworks(user)
804 if err != nil {
805 return err
806 }
giod8ab4f52024-07-26 16:58:34 +0400807 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400808 if !ok {
809 return fmt.Errorf("network not found: %s\n", network)
810 }
gio33059762024-07-05 13:19:07 +0400811 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400812 return err
gio33059762024-07-05 13:19:07 +0400813 }
814 appRepo, err := s.client.GetRepo(appName)
815 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400816 return err
gio33059762024-07-05 13:19:07 +0400817 }
giob4a3a192024-08-19 09:55:47 +0400818 commit, err := s.initRepo(appRepo, appType, n, subdomain)
819 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400820 return err
gio33059762024-07-05 13:19:07 +0400821 }
822 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400823 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
824 if err != nil {
825 return err
826 }
827 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400828 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400829 return err
gio33059762024-07-05 13:19:07 +0400830 }
831 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
832 suffix, err := suffixGen.Generate()
833 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400834 return err
gio33059762024-07-05 13:19:07 +0400835 }
gio94904702024-07-26 16:58:34 +0400836 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400837 s.appConfigs[appName] = appConfig{namespace, network}
giob4a3a192024-08-19 09:55:47 +0400838 resources, err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks)
839 if err != nil {
840 return err
841 }
842 if err = s.createCommit(appName, commit, initCommitMsg, err, resources); err != nil {
843 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +0400844 return err
gio33059762024-07-05 13:19:07 +0400845 }
giod8ab4f52024-07-26 16:58:34 +0400846 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400847 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400848 return err
gio33059762024-07-05 13:19:07 +0400849 }
850 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400851 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400852 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400853 return err
gio33059762024-07-05 13:19:07 +0400854 }
giob4a3a192024-08-19 09:55:47 +0400855 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +0400856 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400857 if err != nil {
858 return "", err
859 }
860 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400861 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400862 return "", err
863 }
864 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400865 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400866 appName,
867 "/"+appName,
868 namespace,
869 map[string]any{
870 "repoAddr": s.client.GetRepoAddress(appName),
871 "repoHost": strings.Split(s.client.Address(), ":")[0],
872 "gitRepoPublicKey": s.gitRepoPublicKey,
873 },
874 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400875 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400876 installer.WithNoPublish(),
877 installer.WithNoLock(),
878 ); err != nil {
879 return "", err
880 }
881 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +0400882 })
883 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400884 return err
gio33059762024-07-05 13:19:07 +0400885 }
886 cfg, err := m.FindInstance(appName)
887 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400888 return err
gio33059762024-07-05 13:19:07 +0400889 }
890 fluxKeys, ok := cfg.Input["fluxKeys"]
891 if !ok {
gio11617ac2024-07-15 16:09:04 +0400892 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400893 }
894 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
895 if !ok {
gio11617ac2024-07-15 16:09:04 +0400896 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400897 }
898 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400899 return err
gio33059762024-07-05 13:19:07 +0400900 } else if ok {
901 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400902 return err
gio33059762024-07-05 13:19:07 +0400903 }
904 } else {
905 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400906 return err
gio33059762024-07-05 13:19:07 +0400907 }
908 }
909 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400910 return err
gio33059762024-07-05 13:19:07 +0400911 }
912 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 +0400913 return err
gio33059762024-07-05 13:19:07 +0400914 }
gio81246f02024-07-10 12:02:15 +0400915 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400916 return err
gio33059762024-07-05 13:19:07 +0400917 }
gio2ccb6e32024-08-15 12:01:33 +0400918 if !s.external {
919 go func() {
920 users, err := s.client.GetAllUsers()
921 if err != nil {
922 fmt.Println(err)
923 return
924 }
925 for _, user := range users {
926 // TODO(gio): fluxcd should have only read access
927 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
928 fmt.Println(err)
929 }
930 }
931 }()
932 }
gio11617ac2024-07-15 16:09:04 +0400933 return nil
gio33059762024-07-05 13:19:07 +0400934}
935
gio81246f02024-07-10 12:02:15 +0400936type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400937 Public string `json:"public"`
938}
939
gio8fae3af2024-07-25 13:43:31 +0400940func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400941 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400942 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
943 http.Error(w, err.Error(), http.StatusBadRequest)
944 return
945 }
946 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
947 http.Error(w, err.Error(), http.StatusInternalServerError)
948 return
949 }
950}
951
gio94904702024-07-26 16:58:34 +0400952type dodoAppRendered struct {
953 App struct {
954 Ingress struct {
955 Network string `json:"network"`
956 Subdomain string `json:"subdomain"`
957 } `json:"ingress"`
958 } `json:"app"`
959 Input struct {
960 AppId string `json:"appId"`
961 } `json:"input"`
962}
963
giob4a3a192024-08-19 09:55:47 +0400964func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) (installer.ReleaseResources, error) {
gio1bf00802024-08-17 12:31:41 +0400965 fmt.Println("111")
gio33059762024-07-05 13:19:07 +0400966 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400967 if err != nil {
giob4a3a192024-08-19 09:55:47 +0400968 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +0400969 }
gio1bf00802024-08-17 12:31:41 +0400970 fmt.Println("111")
giof8843412024-05-22 16:38:05 +0400971 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400972 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400973 if err != nil {
giob4a3a192024-08-19 09:55:47 +0400974 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +0400975 }
gio1bf00802024-08-17 12:31:41 +0400976 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +0400977 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400978 if err != nil {
giob4a3a192024-08-19 09:55:47 +0400979 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +0400980 }
gio1bf00802024-08-17 12:31:41 +0400981 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +0400982 app, err := installer.NewDodoApp(appCfg)
983 if err != nil {
giob4a3a192024-08-19 09:55:47 +0400984 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +0400985 }
gio1bf00802024-08-17 12:31:41 +0400986 fmt.Println("111")
giof8843412024-05-22 16:38:05 +0400987 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +0400988 var ret installer.ReleaseResources
989 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
990 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +0400991 app,
992 "app",
993 "/.dodo/app",
994 namespace,
995 map[string]any{
996 "repoAddr": repo.FullAddress(),
997 "managerAddr": fmt.Sprintf("http://%s", s.self),
998 "appId": name,
999 "sshPrivateKey": s.sshKey,
1000 },
1001 installer.WithNoPull(),
1002 installer.WithNoPublish(),
1003 installer.WithConfig(&s.env),
1004 installer.WithNetworks(networks),
1005 installer.WithLocalChartGenerator(lg),
1006 installer.WithNoLock(),
1007 )
gio1bf00802024-08-17 12:31:41 +04001008 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001009 if err != nil {
1010 return "", err
1011 }
gio1bf00802024-08-17 12:31:41 +04001012 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001013 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001014 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001015 return "", nil
1016 }
gio1bf00802024-08-17 12:31:41 +04001017 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001018 if _, err := m.Install(
1019 appStatus,
1020 "status",
1021 "/.dodo/status",
1022 s.namespace,
1023 map[string]any{
1024 "appName": rendered.Input.AppId,
1025 "network": rendered.App.Ingress.Network,
1026 "appSubdomain": rendered.App.Ingress.Subdomain,
1027 },
1028 installer.WithNoPull(),
1029 installer.WithNoPublish(),
1030 installer.WithConfig(&s.env),
1031 installer.WithNetworks(networks),
1032 installer.WithLocalChartGenerator(lg),
1033 installer.WithNoLock(),
1034 ); err != nil {
1035 return "", err
1036 }
gio1bf00802024-08-17 12:31:41 +04001037 fmt.Println("111")
gio94904702024-07-26 16:58:34 +04001038 return "install app", nil
1039 },
1040 soft.WithCommitToBranch("dodo"),
1041 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001042 ); err != nil {
1043 return installer.ReleaseResources{}, err
1044 }
1045 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001046}
gio33059762024-07-05 13:19:07 +04001047
giob4a3a192024-08-19 09:55:47 +04001048func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) (string, error) {
giob54db242024-07-30 18:49:33 +04001049 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001050 appTmpl, err := s.appTmpls.Find(appType)
1051 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001052 return "", err
gio33059762024-07-05 13:19:07 +04001053 }
gio33059762024-07-05 13:19:07 +04001054 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +04001055 if err := appTmpl.Render(network, subdomain, repo); err != nil {
1056 return "", err
gio33059762024-07-05 13:19:07 +04001057 }
giob4a3a192024-08-19 09:55:47 +04001058 return initCommitMsg, nil
gio33059762024-07-05 13:19:07 +04001059 })
1060}
gio81246f02024-07-10 12:02:15 +04001061
1062func generatePassword() string {
1063 return "foo"
1064}
giocb34ad22024-07-11 08:01:13 +04001065
gio11617ac2024-07-15 16:09:04 +04001066func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001067 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001068 resp, err := http.Get(addr)
1069 if err != nil {
1070 return nil, err
1071 }
gio23bdc1b2024-07-11 16:07:47 +04001072 networks := []installer.Network{}
1073 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001074 return nil, err
1075 }
gio11617ac2024-07-15 16:09:04 +04001076 return s.nf.Filter(user, networks)
1077}
1078
gio8fae3af2024-07-25 13:43:31 +04001079type publicNetworkData struct {
1080 Name string `json:"name"`
1081 Domain string `json:"domain"`
1082}
1083
1084type publicData struct {
1085 Networks []publicNetworkData `json:"networks"`
1086 Types []string `json:"types"`
1087}
1088
1089func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001090 w.Header().Set("Access-Control-Allow-Origin", "*")
1091 s.l.Lock()
1092 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001093 networks, err := s.getNetworks("")
1094 if err != nil {
1095 http.Error(w, err.Error(), http.StatusInternalServerError)
1096 return
1097 }
1098 var ret publicData
1099 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001100 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1101 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1102 }
gio8fae3af2024-07-25 13:43:31 +04001103 }
1104 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001105 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001106 }
gio8fae3af2024-07-25 13:43:31 +04001107 if err := json.NewEncoder(w).Encode(ret); err != nil {
1108 http.Error(w, err.Error(), http.StatusInternalServerError)
1109 return
1110 }
1111}
1112
giob4a3a192024-08-19 09:55:47 +04001113func (s *DodoAppServer) createCommit(name, hash, message string, err error, resources installer.ReleaseResources) error {
1114 if err != nil {
1115 fmt.Printf("Error: %s\n", err.Error())
1116 if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
1117 fmt.Printf("Error: %s\n", err.Error())
1118 return err
1119 }
1120 return err
1121 }
1122 var resB bytes.Buffer
1123 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
1124 if err := s.st.CreateCommit(name, hash, message, "FAILED", err.Error(), nil); err != nil {
1125 fmt.Printf("Error: %s\n", err.Error())
1126 return err
1127 }
1128 return err
1129 }
1130 if err := s.st.CreateCommit(name, hash, message, "OK", "", resB.Bytes()); err != nil {
1131 fmt.Printf("Error: %s\n", err.Error())
1132 return err
1133 }
1134 return nil
1135}
1136
gio11617ac2024-07-15 16:09:04 +04001137func pickNetwork(networks []installer.Network, network string) []installer.Network {
1138 for _, n := range networks {
1139 if n.Name == network {
1140 return []installer.Network{n}
1141 }
1142 }
1143 return []installer.Network{}
1144}
1145
1146type NetworkFilter interface {
1147 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1148}
1149
1150type noNetworkFilter struct{}
1151
1152func NewNoNetworkFilter() NetworkFilter {
1153 return noNetworkFilter{}
1154}
1155
gio8fae3af2024-07-25 13:43:31 +04001156func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001157 return networks, nil
1158}
1159
1160type filterByOwner struct {
1161 st Store
1162}
1163
1164func NewNetworkFilterByOwner(st Store) NetworkFilter {
1165 return &filterByOwner{st}
1166}
1167
1168func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001169 if user == "" {
1170 return networks, nil
1171 }
gio11617ac2024-07-15 16:09:04 +04001172 network, err := f.st.GetUserNetwork(user)
1173 if err != nil {
1174 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001175 }
1176 ret := []installer.Network{}
1177 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001178 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001179 ret = append(ret, n)
1180 }
1181 }
giocb34ad22024-07-11 08:01:13 +04001182 return ret, nil
1183}
gio11617ac2024-07-15 16:09:04 +04001184
1185type allowListFilter struct {
1186 allowed []string
1187}
1188
1189func NewAllowListFilter(allowed []string) NetworkFilter {
1190 return &allowListFilter{allowed}
1191}
1192
gio8fae3af2024-07-25 13:43:31 +04001193func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001194 ret := []installer.Network{}
1195 for _, n := range networks {
1196 if slices.Contains(f.allowed, n.Name) {
1197 ret = append(ret, n)
1198 }
1199 }
1200 return ret, nil
1201}
1202
1203type combinedNetworkFilter struct {
1204 filters []NetworkFilter
1205}
1206
1207func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1208 return &combinedNetworkFilter{filters}
1209}
1210
gio8fae3af2024-07-25 13:43:31 +04001211func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001212 ret := networks
1213 var err error
1214 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001215 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001216 if err != nil {
1217 return nil, err
1218 }
1219 }
1220 return ret, nil
1221}
giocafd4e62024-07-31 10:53:40 +04001222
1223type user struct {
1224 Username string `json:"username"`
1225 Email string `json:"email"`
1226 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1227}
1228
1229func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1230 go s.syncUsers()
1231}
1232
1233func (s *DodoAppServer) syncUsers() {
1234 if s.external {
1235 panic("MUST NOT REACH!")
1236 }
1237 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1238 if err != nil {
1239 return
1240 }
1241 users := []user{}
1242 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1243 fmt.Println(err)
1244 return
1245 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001246 validUsernames := make(map[string]user)
1247 for _, u := range users {
1248 validUsernames[u.Username] = u
1249 }
1250 allClientUsers, err := s.client.GetAllUsers()
1251 if err != nil {
1252 fmt.Println(err)
1253 return
1254 }
1255 keyToUser := make(map[string]string)
1256 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001257 if clientUser == "admin" || clientUser == "fluxcd" {
1258 continue
1259 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001260 userData, ok := validUsernames[clientUser]
1261 if !ok {
1262 if err := s.client.RemoveUser(clientUser); err != nil {
1263 fmt.Println(err)
1264 return
1265 }
1266 } else {
1267 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1268 if err != nil {
1269 fmt.Println(err)
1270 return
1271 }
1272 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001273 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001274 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001275 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001276 })
1277 if !keyOk {
1278 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1279 fmt.Println(err)
1280 }
1281 } else {
1282 keyToUser[cleanKey] = clientUser
1283 }
1284 }
1285 }
1286 }
giocafd4e62024-07-31 10:53:40 +04001287 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001288 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1289 fmt.Println(err)
1290 return
1291 }
giocafd4e62024-07-31 10:53:40 +04001292 if len(u.SSHPublicKeys) == 0 {
1293 continue
1294 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001295 ok, err := s.client.UserExists(u.Username)
1296 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001297 fmt.Println(err)
1298 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001299 }
1300 if !ok {
1301 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1302 fmt.Println(err)
1303 return
1304 }
1305 } else {
1306 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001307 cleanKey := soft.CleanKey(key)
1308 if user, ok := keyToUser[cleanKey]; ok {
1309 if u.Username != user {
1310 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1311 }
1312 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001313 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001314 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001315 fmt.Println(err)
1316 return
giocafd4e62024-07-31 10:53:40 +04001317 }
1318 }
1319 }
1320 }
1321 repos, err := s.client.GetAllRepos()
1322 if err != nil {
1323 return
1324 }
1325 for _, r := range repos {
1326 if r == ConfigRepoName {
1327 continue
1328 }
1329 for _, u := range users {
1330 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1331 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001332 continue
giocafd4e62024-07-31 10:53:40 +04001333 }
1334 }
1335 }
1336}
giob4a3a192024-08-19 09:55:47 +04001337
1338func extractResourceData(resources []installer.Resource) (resourceData, error) {
1339 var ret resourceData
1340 for _, r := range resources {
1341 t, ok := r.Annotations["dodo.cloud/resource-type"]
1342 if !ok {
1343 continue
1344 }
1345 switch t {
1346 case "volume":
1347 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1348 if !ok {
1349 return resourceData{}, fmt.Errorf("no name")
1350 }
1351 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1352 if !ok {
1353 return resourceData{}, fmt.Errorf("no size")
1354 }
1355 ret.Volume = append(ret.Volume, volume{name, size})
1356 case "postgresql":
1357 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1358 if !ok {
1359 return resourceData{}, fmt.Errorf("no name")
1360 }
1361 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1362 if !ok {
1363 return resourceData{}, fmt.Errorf("no version")
1364 }
1365 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1366 if !ok {
1367 return resourceData{}, fmt.Errorf("no volume")
1368 }
1369 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1370 case "ingress":
1371 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1372 if !ok {
1373 return resourceData{}, fmt.Errorf("no host")
1374 }
1375 ret.Ingress = append(ret.Ingress, ingress{host})
1376 default:
1377 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1378 }
1379 }
1380 return ret, nil
1381}