blob: 7c35d696dd8ca48b9847cd9bca6e12003f10a9f1 [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"
gio9d66f322024-07-06 13:45:10 +040045)
46
gio23bdc1b2024-07-11 16:07:47 +040047type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040048 index *template.Template
49 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040050}
51
52func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040053 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040054 if err != nil {
55 return dodoAppTmplts{}, err
56 }
gio5e49bb62024-07-20 10:43:19 +040057 parse := func(path string) (*template.Template, error) {
58 if b, err := base.Clone(); err != nil {
59 return nil, err
60 } else {
61 return b.ParseFS(fs, path)
62 }
63 }
64 index, err := parse("dodo-app-tmpl/index.html")
65 if err != nil {
66 return dodoAppTmplts{}, err
67 }
68 appStatus, err := parse("dodo-app-tmpl/app_status.html")
69 if err != nil {
70 return dodoAppTmplts{}, err
71 }
72 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040073}
74
gio0eaf2712024-04-14 13:08:46 +040075type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040076 l sync.Locker
77 st Store
gio11617ac2024-07-15 16:09:04 +040078 nf NetworkFilter
79 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040080 port int
81 apiPort int
82 self string
gio11617ac2024-07-15 16:09:04 +040083 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040084 sshKey string
85 gitRepoPublicKey string
86 client soft.Client
87 namespace string
88 envAppManagerAddr string
89 env installer.EnvConfig
90 nsc installer.NamespaceCreator
91 jc installer.JobCreator
92 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040093 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +040094 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040095 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +040096 external bool
97 fetchUsersAddr string
giod8ab4f52024-07-26 16:58:34 +040098}
99
100type appConfig struct {
101 Namespace string `json:"namespace"`
102 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400103}
104
gio33059762024-07-05 13:19:07 +0400105// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400106func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400107 st Store,
gio11617ac2024-07-15 16:09:04 +0400108 nf NetworkFilter,
109 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400110 port int,
gioa60f0de2024-07-08 10:49:48 +0400111 apiPort int,
gio33059762024-07-05 13:19:07 +0400112 self string,
gio11617ac2024-07-15 16:09:04 +0400113 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400114 sshKey string,
gio33059762024-07-05 13:19:07 +0400115 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400116 client soft.Client,
117 namespace string,
giocb34ad22024-07-11 08:01:13 +0400118 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400119 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400120 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400121 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400122 external bool,
123 fetchUsersAddr string,
gio9d66f322024-07-06 13:45:10 +0400124) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400125 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
126 if err != nil {
127 return nil, err
128 }
gio5e49bb62024-07-20 10:43:19 +0400129 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
130 if err != nil {
131 return nil, err
132 }
133 appTmpls, err := NewAppTmplStoreFS(apps)
134 if err != nil {
135 return nil, err
136 }
gio9d66f322024-07-06 13:45:10 +0400137 s := &DodoAppServer{
138 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400139 st,
gio11617ac2024-07-15 16:09:04 +0400140 nf,
141 ug,
gio0eaf2712024-04-14 13:08:46 +0400142 port,
gioa60f0de2024-07-08 10:49:48 +0400143 apiPort,
gio33059762024-07-05 13:19:07 +0400144 self,
gio11617ac2024-07-15 16:09:04 +0400145 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400146 sshKey,
gio33059762024-07-05 13:19:07 +0400147 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400148 client,
149 namespace,
giocb34ad22024-07-11 08:01:13 +0400150 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400151 env,
gio33059762024-07-05 13:19:07 +0400152 nsc,
giof8843412024-05-22 16:38:05 +0400153 jc,
gio266c04f2024-07-03 14:18:45 +0400154 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400155 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400156 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400157 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400158 external,
159 fetchUsersAddr,
gio0eaf2712024-04-14 13:08:46 +0400160 }
gioa60f0de2024-07-08 10:49:48 +0400161 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400162 if err != nil {
163 return nil, err
164 }
giod8ab4f52024-07-26 16:58:34 +0400165 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400166 if err == nil {
167 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400168 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400169 return nil, err
170 }
171 } else if !errors.Is(err, fs.ErrNotExist) {
172 return nil, err
173 }
174 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400175}
176
177func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400178 e := make(chan error)
179 go func() {
180 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400181 r.Use(s.mwAuth)
gio1bf00802024-08-17 12:31:41 +0400182 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400183 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400184 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
185 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400186 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
187 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
188 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
189 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400190 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400191 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
192 }()
193 go func() {
194 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400195 r.HandleFunc("/update", s.handleAPIUpdate)
196 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
197 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
giocafd4e62024-07-31 10:53:40 +0400198 if !s.external {
199 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
200 }
gioa60f0de2024-07-08 10:49:48 +0400201 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
202 }()
giocafd4e62024-07-31 10:53:40 +0400203 if !s.external {
204 go func() {
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400205 rand.Seed(uint64(time.Now().UnixNano()))
giocafd4e62024-07-31 10:53:40 +0400206 s.syncUsers()
207 // TODO(dtabidze): every sync delay should be randomized to avoid all client
208 // applications hitting memberships service at the same time.
209 // For every next sync new delay should be randomly generated from scratch.
210 // We can choose random delay from 1 to 2 minutes.
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400211 // for range time.Tick(1 * time.Minute) {
212 // s.syncUsers()
213 // }
214 for {
215 delay := time.Duration(rand.Intn(60)+60) * time.Second
216 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400217 s.syncUsers()
218 }
219 }()
220 }
gioa60f0de2024-07-08 10:49:48 +0400221 return <-e
222}
223
gio11617ac2024-07-15 16:09:04 +0400224type UserGetter interface {
225 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400226 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400227}
228
229type externalUserGetter struct {
230 sc *securecookie.SecureCookie
231}
232
233func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400234 return &externalUserGetter{securecookie.New(
235 securecookie.GenerateRandomKey(64),
236 securecookie.GenerateRandomKey(32),
237 )}
gio11617ac2024-07-15 16:09:04 +0400238}
239
240func (ug *externalUserGetter) Get(r *http.Request) string {
241 cookie, err := r.Cookie(sessionCookie)
242 if err != nil {
243 return ""
244 }
245 var user string
246 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
247 return ""
248 }
249 return user
250}
251
gio8fae3af2024-07-25 13:43:31 +0400252func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
253 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
254 cookie := &http.Cookie{
255 Name: sessionCookie,
256 Value: encoded,
257 Path: "/",
258 Secure: true,
259 HttpOnly: true,
260 }
261 http.SetCookie(w, cookie)
262 return nil
263 } else {
264 return err
265 }
266}
267
gio11617ac2024-07-15 16:09:04 +0400268type internalUserGetter struct{}
269
270func NewInternalUserGetter() UserGetter {
271 return internalUserGetter{}
272}
273
274func (ug internalUserGetter) Get(r *http.Request) string {
275 return r.Header.Get("X-User")
276}
277
gio8fae3af2024-07-25 13:43:31 +0400278func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
279 return nil
280}
281
gio81246f02024-07-10 12:02:15 +0400282func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
283 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400284 if strings.HasSuffix(r.URL.Path, loginPath) ||
285 strings.HasPrefix(r.URL.Path, logoutPath) ||
286 strings.HasPrefix(r.URL.Path, staticPath) ||
287 strings.HasPrefix(r.URL.Path, apiPublicData) ||
288 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400289 next.ServeHTTP(w, r)
290 return
291 }
gio11617ac2024-07-15 16:09:04 +0400292 user := s.ug.Get(r)
293 if user == "" {
gio81246f02024-07-10 12:02:15 +0400294 vars := mux.Vars(r)
295 appName, ok := vars["app-name"]
296 if !ok || appName == "" {
297 http.Error(w, "missing app-name", http.StatusBadRequest)
298 return
299 }
300 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
301 return
302 }
gio81246f02024-07-10 12:02:15 +0400303 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
304 })
305}
306
307func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400308 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400309 http.SetCookie(w, &http.Cookie{
310 Name: sessionCookie,
311 Value: "",
312 Path: "/",
313 HttpOnly: true,
314 Secure: true,
315 })
316 http.Redirect(w, r, "/", http.StatusSeeOther)
317}
318
319func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
320 vars := mux.Vars(r)
321 appName, ok := vars["app-name"]
322 if !ok || appName == "" {
323 http.Error(w, "missing app-name", http.StatusBadRequest)
324 return
325 }
326 fmt.Fprint(w, `
327<!DOCTYPE html>
328<html lang='en'>
329 <head>
330 <title>dodo: app - login</title>
331 <meta charset='utf-8'>
332 </head>
333 <body>
334 <form action="" method="POST">
335 <input type="password" placeholder="Password" name="password" required />
336 <button type="submit">Login</button>
337 </form>
338 </body>
339</html>
340`)
341}
342
343func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
344 vars := mux.Vars(r)
345 appName, ok := vars["app-name"]
346 if !ok || appName == "" {
347 http.Error(w, "missing app-name", http.StatusBadRequest)
348 return
349 }
350 password := r.FormValue("password")
351 if password == "" {
352 http.Error(w, "missing password", http.StatusBadRequest)
353 return
354 }
355 user, err := s.st.GetAppOwner(appName)
356 if err != nil {
357 http.Error(w, err.Error(), http.StatusInternalServerError)
358 return
359 }
360 hashed, err := s.st.GetUserPassword(user)
361 if err != nil {
362 http.Error(w, err.Error(), http.StatusInternalServerError)
363 return
364 }
365 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
366 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
367 return
368 }
gio8fae3af2024-07-25 13:43:31 +0400369 if err := s.ug.Encode(w, user); err != nil {
370 http.Error(w, err.Error(), http.StatusInternalServerError)
371 return
gio81246f02024-07-10 12:02:15 +0400372 }
373 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
374}
375
gio23bdc1b2024-07-11 16:07:47 +0400376type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400377 Apps []string
378 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400379 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400380}
381
gioa60f0de2024-07-08 10:49:48 +0400382func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400383 user := r.Context().Value(userCtx)
384 if user == nil {
385 http.Error(w, "unauthorized", http.StatusUnauthorized)
386 return
387 }
388 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400389 if err != nil {
390 http.Error(w, err.Error(), http.StatusInternalServerError)
391 return
392 }
gio11617ac2024-07-15 16:09:04 +0400393 networks, err := s.getNetworks(user.(string))
394 if err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
397 }
giob54db242024-07-30 18:49:33 +0400398 var types []string
399 for _, t := range s.appTmpls.Types() {
400 types = append(types, strings.Replace(t, "-", ":", 1))
401 }
gio5e49bb62024-07-20 10:43:19 +0400402 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400403 if err := s.tmplts.index.Execute(w, data); err != nil {
404 http.Error(w, err.Error(), http.StatusInternalServerError)
405 return
gioa60f0de2024-07-08 10:49:48 +0400406 }
407}
408
gio5e49bb62024-07-20 10:43:19 +0400409type appStatusData struct {
410 Name string
411 GitCloneCommand string
412 Commits []Commit
413}
414
gioa60f0de2024-07-08 10:49:48 +0400415func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
416 vars := mux.Vars(r)
417 appName, ok := vars["app-name"]
418 if !ok || appName == "" {
419 http.Error(w, "missing app-name", http.StatusBadRequest)
420 return
421 }
gio94904702024-07-26 16:58:34 +0400422 u := r.Context().Value(userCtx)
423 if u == nil {
424 http.Error(w, "unauthorized", http.StatusUnauthorized)
425 return
426 }
427 user, ok := u.(string)
428 if !ok {
429 http.Error(w, "could not get user", http.StatusInternalServerError)
430 return
431 }
432 owner, err := s.st.GetAppOwner(appName)
433 if err != nil {
434 http.Error(w, err.Error(), http.StatusInternalServerError)
435 return
436 }
437 if owner != user {
438 http.Error(w, "unauthorized", http.StatusUnauthorized)
439 return
440 }
gioa60f0de2024-07-08 10:49:48 +0400441 commits, err := s.st.GetCommitHistory(appName)
442 if err != nil {
443 http.Error(w, err.Error(), http.StatusInternalServerError)
444 return
445 }
gio5e49bb62024-07-20 10:43:19 +0400446 data := appStatusData{
447 Name: appName,
448 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
449 Commits: commits,
450 }
451 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
452 http.Error(w, err.Error(), http.StatusInternalServerError)
453 return
gioa60f0de2024-07-08 10:49:48 +0400454 }
gio0eaf2712024-04-14 13:08:46 +0400455}
456
gio81246f02024-07-10 12:02:15 +0400457type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400458 Ref string `json:"ref"`
459 Repository struct {
460 Name string `json:"name"`
461 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400462 After string `json:"after"`
463 Commits []struct {
464 Id string `json:"id"`
465 Message string `json:"message"`
466 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400467}
468
gio8fae3af2024-07-25 13:43:31 +0400469func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400470 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400471 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400472 var contents strings.Builder
473 io.Copy(&contents, r.Body)
474 c := contents.String()
475 fmt.Println(c)
476 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400477 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400478 return
479 }
gioa60f0de2024-07-08 10:49:48 +0400480 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400481 return
482 }
gioa60f0de2024-07-08 10:49:48 +0400483 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400484 go func() {
gio11617ac2024-07-15 16:09:04 +0400485 owner, err := s.st.GetAppOwner(req.Repository.Name)
486 if err != nil {
487 return
488 }
489 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400490 if err != nil {
491 return
492 }
gio94904702024-07-26 16:58:34 +0400493 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
494 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
495 if err != nil {
496 return
497 }
gioe2e31e12024-08-18 08:20:56 +0400498 found := false
499 commitMsg := ""
500 for _, c := range req.Commits {
501 if c.Id == req.After {
502 found = true
503 commitMsg = c.Message
504 break
gioa60f0de2024-07-08 10:49:48 +0400505 }
506 }
gioe2e31e12024-08-18 08:20:56 +0400507 if !found {
508 fmt.Printf("Error: could not find commit message")
509 return
510 }
511 if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
gio12e887d2024-08-18 16:09:47 +0400512 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400513 if err := s.st.CreateCommit(req.Repository.Name, req.After, commitMsg, err.Error()); err != nil {
514 fmt.Printf("Error: %s\n", err.Error())
515 }
516 return
517 }
518 if err := s.st.CreateCommit(req.Repository.Name, req.After, commitMsg, "OK"); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400519 fmt.Printf("Error: %s\n", err.Error())
520 }
521 for addr, _ := range s.workers[req.Repository.Name] {
522 go func() {
523 // TODO(gio): make port configurable
524 http.Get(fmt.Sprintf("http://%s/update", addr))
525 }()
gio0eaf2712024-04-14 13:08:46 +0400526 }
527 }()
gio0eaf2712024-04-14 13:08:46 +0400528}
529
gio81246f02024-07-10 12:02:15 +0400530type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400531 Address string `json:"address"`
532}
533
gio8fae3af2024-07-25 13:43:31 +0400534func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400535 vars := mux.Vars(r)
536 appName, ok := vars["app-name"]
537 if !ok || appName == "" {
538 http.Error(w, "missing app-name", http.StatusBadRequest)
539 return
540 }
gio81246f02024-07-10 12:02:15 +0400541 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400542 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
543 http.Error(w, err.Error(), http.StatusInternalServerError)
544 return
545 }
gioa60f0de2024-07-08 10:49:48 +0400546 if _, ok := s.workers[appName]; !ok {
547 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400548 }
gioa60f0de2024-07-08 10:49:48 +0400549 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400550}
551
gio11617ac2024-07-15 16:09:04 +0400552func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
553 u := r.Context().Value(userCtx)
554 if u == nil {
555 http.Error(w, "unauthorized", http.StatusUnauthorized)
556 return
557 }
558 user, ok := u.(string)
559 if !ok {
560 http.Error(w, "could not get user", http.StatusInternalServerError)
561 return
562 }
563 network := r.FormValue("network")
564 if network == "" {
565 http.Error(w, "missing network", http.StatusBadRequest)
566 return
567 }
gio5e49bb62024-07-20 10:43:19 +0400568 subdomain := r.FormValue("subdomain")
569 if subdomain == "" {
570 http.Error(w, "missing subdomain", http.StatusBadRequest)
571 return
572 }
573 appType := r.FormValue("type")
574 if appType == "" {
575 http.Error(w, "missing type", http.StatusBadRequest)
576 return
577 }
gio11617ac2024-07-15 16:09:04 +0400578 g := installer.NewFixedLengthRandomNameGenerator(3)
579 appName, err := g.Generate()
580 if err != nil {
581 http.Error(w, err.Error(), http.StatusInternalServerError)
582 return
583 }
584 if ok, err := s.client.UserExists(user); err != nil {
585 http.Error(w, err.Error(), http.StatusInternalServerError)
586 return
587 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400588 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
589 return
gio11617ac2024-07-15 16:09:04 +0400590 }
giocafd4e62024-07-31 10:53:40 +0400591 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400592 http.Error(w, err.Error(), http.StatusInternalServerError)
593 return
594 }
595 if err := s.st.CreateApp(appName, user); err != nil {
596 http.Error(w, err.Error(), http.StatusInternalServerError)
597 return
598 }
giod8ab4f52024-07-26 16:58:34 +0400599 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400600 http.Error(w, err.Error(), http.StatusInternalServerError)
601 return
602 }
603 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
604}
605
gio81246f02024-07-10 12:02:15 +0400606type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400607 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400608 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400609 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400610 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400611}
612
gio81246f02024-07-10 12:02:15 +0400613type apiCreateAppResp struct {
614 AppName string `json:"appName"`
615 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400616}
617
gio8fae3af2024-07-25 13:43:31 +0400618func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400619 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400620 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400621 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
622 http.Error(w, err.Error(), http.StatusBadRequest)
623 return
624 }
625 g := installer.NewFixedLengthRandomNameGenerator(3)
626 appName, err := g.Generate()
627 if err != nil {
628 http.Error(w, err.Error(), http.StatusInternalServerError)
629 return
630 }
gio11617ac2024-07-15 16:09:04 +0400631 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400632 if err != nil {
gio33059762024-07-05 13:19:07 +0400633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
gio11617ac2024-07-15 16:09:04 +0400636 if user != "" {
637 http.Error(w, "public key already registered", http.StatusBadRequest)
638 return
639 }
640 user = appName
641 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
642 http.Error(w, err.Error(), http.StatusInternalServerError)
643 return
644 }
645 password := generatePassword()
646 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
647 if err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
giocafd4e62024-07-31 10:53:40 +0400651 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400652 http.Error(w, err.Error(), http.StatusInternalServerError)
653 return
654 }
655 if err := s.st.CreateApp(appName, user); err != nil {
656 http.Error(w, err.Error(), http.StatusInternalServerError)
657 return
658 }
giod8ab4f52024-07-26 16:58:34 +0400659 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400660 http.Error(w, err.Error(), http.StatusInternalServerError)
661 return
662 }
gio81246f02024-07-10 12:02:15 +0400663 resp := apiCreateAppResp{
664 AppName: appName,
665 Password: password,
666 }
gio33059762024-07-05 13:19:07 +0400667 if err := json.NewEncoder(w).Encode(resp); err != nil {
668 http.Error(w, err.Error(), http.StatusInternalServerError)
669 return
670 }
671}
672
giod8ab4f52024-07-26 16:58:34 +0400673func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400674 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400675 return true
676 }
677 for _, cfg := range s.appConfigs {
678 if strings.ToLower(cfg.Network) == network {
679 return false
680 }
681 }
682 return true
683}
684
685func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400686 s.l.Lock()
687 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400688 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400689 network = strings.ToLower(network)
690 if !s.isNetworkUseAllowed(network) {
691 return fmt.Errorf("network already used: %s", network)
692 }
gio33059762024-07-05 13:19:07 +0400693 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400694 return err
gio33059762024-07-05 13:19:07 +0400695 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400696 return nil
gioa60f0de2024-07-08 10:49:48 +0400697 }
gio5e49bb62024-07-20 10:43:19 +0400698 networks, err := s.getNetworks(user)
699 if err != nil {
700 return err
701 }
giod8ab4f52024-07-26 16:58:34 +0400702 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400703 if !ok {
704 return fmt.Errorf("network not found: %s\n", network)
705 }
gio33059762024-07-05 13:19:07 +0400706 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400707 return err
gio33059762024-07-05 13:19:07 +0400708 }
709 appRepo, err := s.client.GetRepo(appName)
710 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400711 return err
gio33059762024-07-05 13:19:07 +0400712 }
gio5e49bb62024-07-20 10:43:19 +0400713 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400714 return err
gio33059762024-07-05 13:19:07 +0400715 }
716 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400717 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
718 if err != nil {
719 return err
720 }
721 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400722 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400723 return err
gio33059762024-07-05 13:19:07 +0400724 }
725 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
726 suffix, err := suffixGen.Generate()
727 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400728 return err
gio33059762024-07-05 13:19:07 +0400729 }
gio94904702024-07-26 16:58:34 +0400730 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400731 s.appConfigs[appName] = appConfig{namespace, network}
gio94904702024-07-26 16:58:34 +0400732 if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400733 return err
gio33059762024-07-05 13:19:07 +0400734 }
giod8ab4f52024-07-26 16:58:34 +0400735 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400736 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400737 return err
gio33059762024-07-05 13:19:07 +0400738 }
739 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400740 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400741 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400742 return err
gio33059762024-07-05 13:19:07 +0400743 }
giod8ab4f52024-07-26 16:58:34 +0400744 if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
745 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400746 if err != nil {
747 return "", err
748 }
749 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400750 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400751 return "", err
752 }
753 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400754 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400755 appName,
756 "/"+appName,
757 namespace,
758 map[string]any{
759 "repoAddr": s.client.GetRepoAddress(appName),
760 "repoHost": strings.Split(s.client.Address(), ":")[0],
761 "gitRepoPublicKey": s.gitRepoPublicKey,
762 },
763 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400764 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400765 installer.WithNoPublish(),
766 installer.WithNoLock(),
767 ); err != nil {
768 return "", err
769 }
770 return fmt.Sprintf("Installed app: %s", appName), nil
771 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400772 return err
gio33059762024-07-05 13:19:07 +0400773 }
774 cfg, err := m.FindInstance(appName)
775 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400776 return err
gio33059762024-07-05 13:19:07 +0400777 }
778 fluxKeys, ok := cfg.Input["fluxKeys"]
779 if !ok {
gio11617ac2024-07-15 16:09:04 +0400780 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400781 }
782 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
783 if !ok {
gio11617ac2024-07-15 16:09:04 +0400784 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400785 }
786 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400787 return err
gio33059762024-07-05 13:19:07 +0400788 } else if ok {
789 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400790 return err
gio33059762024-07-05 13:19:07 +0400791 }
792 } else {
793 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400794 return err
gio33059762024-07-05 13:19:07 +0400795 }
796 }
797 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400798 return err
gio33059762024-07-05 13:19:07 +0400799 }
800 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 +0400801 return err
gio33059762024-07-05 13:19:07 +0400802 }
gio81246f02024-07-10 12:02:15 +0400803 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400804 return err
gio33059762024-07-05 13:19:07 +0400805 }
gio2ccb6e32024-08-15 12:01:33 +0400806 if !s.external {
807 go func() {
808 users, err := s.client.GetAllUsers()
809 if err != nil {
810 fmt.Println(err)
811 return
812 }
813 for _, user := range users {
814 // TODO(gio): fluxcd should have only read access
815 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
816 fmt.Println(err)
817 }
818 }
819 }()
820 }
gio11617ac2024-07-15 16:09:04 +0400821 return nil
gio33059762024-07-05 13:19:07 +0400822}
823
gio81246f02024-07-10 12:02:15 +0400824type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400825 Public string `json:"public"`
826}
827
gio8fae3af2024-07-25 13:43:31 +0400828func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400829 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400830 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
831 http.Error(w, err.Error(), http.StatusBadRequest)
832 return
833 }
834 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
835 http.Error(w, err.Error(), http.StatusInternalServerError)
836 return
837 }
838}
839
gio94904702024-07-26 16:58:34 +0400840type dodoAppRendered struct {
841 App struct {
842 Ingress struct {
843 Network string `json:"network"`
844 Subdomain string `json:"subdomain"`
845 } `json:"ingress"`
846 } `json:"app"`
847 Input struct {
848 AppId string `json:"appId"`
849 } `json:"input"`
850}
851
852func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
gio1bf00802024-08-17 12:31:41 +0400853 fmt.Println("111")
gio33059762024-07-05 13:19:07 +0400854 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400855 if err != nil {
856 return err
857 }
gio1bf00802024-08-17 12:31:41 +0400858 fmt.Println("111")
giof8843412024-05-22 16:38:05 +0400859 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400860 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400861 if err != nil {
862 return err
863 }
gio1bf00802024-08-17 12:31:41 +0400864 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +0400865 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400866 if err != nil {
867 return err
868 }
gio1bf00802024-08-17 12:31:41 +0400869 fmt.Println("111")
gio0eaf2712024-04-14 13:08:46 +0400870 app, err := installer.NewDodoApp(appCfg)
871 if err != nil {
872 return err
873 }
gio1bf00802024-08-17 12:31:41 +0400874 fmt.Println("111")
giof8843412024-05-22 16:38:05 +0400875 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
gio94904702024-07-26 16:58:34 +0400876 return repo.Do(func(r soft.RepoFS) (string, error) {
877 res, err := m.Install(
878 app,
879 "app",
880 "/.dodo/app",
881 namespace,
882 map[string]any{
883 "repoAddr": repo.FullAddress(),
884 "managerAddr": fmt.Sprintf("http://%s", s.self),
885 "appId": name,
886 "sshPrivateKey": s.sshKey,
887 },
888 installer.WithNoPull(),
889 installer.WithNoPublish(),
890 installer.WithConfig(&s.env),
891 installer.WithNetworks(networks),
892 installer.WithLocalChartGenerator(lg),
893 installer.WithNoLock(),
894 )
gio1bf00802024-08-17 12:31:41 +0400895 fmt.Println("111")
gio94904702024-07-26 16:58:34 +0400896 if err != nil {
897 return "", err
898 }
gio1bf00802024-08-17 12:31:41 +0400899 fmt.Println("111")
gio94904702024-07-26 16:58:34 +0400900 var rendered dodoAppRendered
901 if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
902 return "", nil
903 }
gio1bf00802024-08-17 12:31:41 +0400904 fmt.Println("111")
gio94904702024-07-26 16:58:34 +0400905 if _, err := m.Install(
906 appStatus,
907 "status",
908 "/.dodo/status",
909 s.namespace,
910 map[string]any{
911 "appName": rendered.Input.AppId,
912 "network": rendered.App.Ingress.Network,
913 "appSubdomain": rendered.App.Ingress.Subdomain,
914 },
915 installer.WithNoPull(),
916 installer.WithNoPublish(),
917 installer.WithConfig(&s.env),
918 installer.WithNetworks(networks),
919 installer.WithLocalChartGenerator(lg),
920 installer.WithNoLock(),
921 ); err != nil {
922 return "", err
923 }
gio1bf00802024-08-17 12:31:41 +0400924 fmt.Println("111")
gio94904702024-07-26 16:58:34 +0400925 return "install app", nil
926 },
927 soft.WithCommitToBranch("dodo"),
928 soft.WithForce(),
929 )
gio0eaf2712024-04-14 13:08:46 +0400930}
gio33059762024-07-05 13:19:07 +0400931
gio5e49bb62024-07-20 10:43:19 +0400932func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
giob54db242024-07-30 18:49:33 +0400933 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +0400934 appTmpl, err := s.appTmpls.Find(appType)
935 if err != nil {
936 return err
gio33059762024-07-05 13:19:07 +0400937 }
gio33059762024-07-05 13:19:07 +0400938 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400939 if err := appTmpl.Render(network, subdomain, repo); err != nil {
940 return "", err
gio33059762024-07-05 13:19:07 +0400941 }
gio5e49bb62024-07-20 10:43:19 +0400942 return "init", nil
gio33059762024-07-05 13:19:07 +0400943 })
944}
gio81246f02024-07-10 12:02:15 +0400945
946func generatePassword() string {
947 return "foo"
948}
giocb34ad22024-07-11 08:01:13 +0400949
gio11617ac2024-07-15 16:09:04 +0400950func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400951 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400952 resp, err := http.Get(addr)
953 if err != nil {
954 return nil, err
955 }
gio23bdc1b2024-07-11 16:07:47 +0400956 networks := []installer.Network{}
957 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400958 return nil, err
959 }
gio11617ac2024-07-15 16:09:04 +0400960 return s.nf.Filter(user, networks)
961}
962
gio8fae3af2024-07-25 13:43:31 +0400963type publicNetworkData struct {
964 Name string `json:"name"`
965 Domain string `json:"domain"`
966}
967
968type publicData struct {
969 Networks []publicNetworkData `json:"networks"`
970 Types []string `json:"types"`
971}
972
973func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400974 w.Header().Set("Access-Control-Allow-Origin", "*")
975 s.l.Lock()
976 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +0400977 networks, err := s.getNetworks("")
978 if err != nil {
979 http.Error(w, err.Error(), http.StatusInternalServerError)
980 return
981 }
982 var ret publicData
983 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +0400984 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
985 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
986 }
gio8fae3af2024-07-25 13:43:31 +0400987 }
988 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +0400989 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +0400990 }
gio8fae3af2024-07-25 13:43:31 +0400991 if err := json.NewEncoder(w).Encode(ret); err != nil {
992 http.Error(w, err.Error(), http.StatusInternalServerError)
993 return
994 }
995}
996
gio11617ac2024-07-15 16:09:04 +0400997func pickNetwork(networks []installer.Network, network string) []installer.Network {
998 for _, n := range networks {
999 if n.Name == network {
1000 return []installer.Network{n}
1001 }
1002 }
1003 return []installer.Network{}
1004}
1005
1006type NetworkFilter interface {
1007 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1008}
1009
1010type noNetworkFilter struct{}
1011
1012func NewNoNetworkFilter() NetworkFilter {
1013 return noNetworkFilter{}
1014}
1015
gio8fae3af2024-07-25 13:43:31 +04001016func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001017 return networks, nil
1018}
1019
1020type filterByOwner struct {
1021 st Store
1022}
1023
1024func NewNetworkFilterByOwner(st Store) NetworkFilter {
1025 return &filterByOwner{st}
1026}
1027
1028func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001029 if user == "" {
1030 return networks, nil
1031 }
gio11617ac2024-07-15 16:09:04 +04001032 network, err := f.st.GetUserNetwork(user)
1033 if err != nil {
1034 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001035 }
1036 ret := []installer.Network{}
1037 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001038 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001039 ret = append(ret, n)
1040 }
1041 }
giocb34ad22024-07-11 08:01:13 +04001042 return ret, nil
1043}
gio11617ac2024-07-15 16:09:04 +04001044
1045type allowListFilter struct {
1046 allowed []string
1047}
1048
1049func NewAllowListFilter(allowed []string) NetworkFilter {
1050 return &allowListFilter{allowed}
1051}
1052
gio8fae3af2024-07-25 13:43:31 +04001053func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001054 ret := []installer.Network{}
1055 for _, n := range networks {
1056 if slices.Contains(f.allowed, n.Name) {
1057 ret = append(ret, n)
1058 }
1059 }
1060 return ret, nil
1061}
1062
1063type combinedNetworkFilter struct {
1064 filters []NetworkFilter
1065}
1066
1067func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1068 return &combinedNetworkFilter{filters}
1069}
1070
gio8fae3af2024-07-25 13:43:31 +04001071func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001072 ret := networks
1073 var err error
1074 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001075 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001076 if err != nil {
1077 return nil, err
1078 }
1079 }
1080 return ret, nil
1081}
giocafd4e62024-07-31 10:53:40 +04001082
1083type user struct {
1084 Username string `json:"username"`
1085 Email string `json:"email"`
1086 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1087}
1088
1089func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1090 go s.syncUsers()
1091}
1092
1093func (s *DodoAppServer) syncUsers() {
1094 if s.external {
1095 panic("MUST NOT REACH!")
1096 }
1097 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1098 if err != nil {
1099 return
1100 }
1101 users := []user{}
1102 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1103 fmt.Println(err)
1104 return
1105 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001106 validUsernames := make(map[string]user)
1107 for _, u := range users {
1108 validUsernames[u.Username] = u
1109 }
1110 allClientUsers, err := s.client.GetAllUsers()
1111 if err != nil {
1112 fmt.Println(err)
1113 return
1114 }
1115 keyToUser := make(map[string]string)
1116 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001117 if clientUser == "admin" || clientUser == "fluxcd" {
1118 continue
1119 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001120 userData, ok := validUsernames[clientUser]
1121 if !ok {
1122 if err := s.client.RemoveUser(clientUser); err != nil {
1123 fmt.Println(err)
1124 return
1125 }
1126 } else {
1127 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1128 if err != nil {
1129 fmt.Println(err)
1130 return
1131 }
1132 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001133 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001134 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001135 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001136 })
1137 if !keyOk {
1138 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1139 fmt.Println(err)
1140 }
1141 } else {
1142 keyToUser[cleanKey] = clientUser
1143 }
1144 }
1145 }
1146 }
giocafd4e62024-07-31 10:53:40 +04001147 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001148 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1149 fmt.Println(err)
1150 return
1151 }
giocafd4e62024-07-31 10:53:40 +04001152 if len(u.SSHPublicKeys) == 0 {
1153 continue
1154 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001155 ok, err := s.client.UserExists(u.Username)
1156 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001157 fmt.Println(err)
1158 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001159 }
1160 if !ok {
1161 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1162 fmt.Println(err)
1163 return
1164 }
1165 } else {
1166 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001167 cleanKey := soft.CleanKey(key)
1168 if user, ok := keyToUser[cleanKey]; ok {
1169 if u.Username != user {
1170 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1171 }
1172 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001173 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001174 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001175 fmt.Println(err)
1176 return
giocafd4e62024-07-31 10:53:40 +04001177 }
1178 }
1179 }
1180 }
1181 repos, err := s.client.GetAllRepos()
1182 if err != nil {
1183 return
1184 }
1185 for _, r := range repos {
1186 if r == ConfigRepoName {
1187 continue
1188 }
1189 for _, u := range users {
1190 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1191 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001192 continue
giocafd4e62024-07-31 10:53:40 +04001193 }
1194 }
1195 }
1196}