blob: 32fb0de7ebd5c7201bb718354fbb6f90b911d323 [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
35//go:embed static
36var staticResources embed.FS
37
gio9d66f322024-07-06 13:45:10 +040038const (
gioa60f0de2024-07-08 10:49:48 +040039 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040040 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040041 loginPath = "/login"
42 logoutPath = "/logout"
gio5e49bb62024-07-20 10:43:19 +040043 staticPath = "/static"
gio8fae3af2024-07-25 13:43:31 +040044 apiPublicData = "/api/public-data"
45 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040046 sessionCookie = "dodo-app-session"
47 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040048)
49
gio23bdc1b2024-07-11 16:07:47 +040050type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040051 index *template.Template
52 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040053}
54
55func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040056 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040057 if err != nil {
58 return dodoAppTmplts{}, err
59 }
gio5e49bb62024-07-20 10:43:19 +040060 parse := func(path string) (*template.Template, error) {
61 if b, err := base.Clone(); err != nil {
62 return nil, err
63 } else {
64 return b.ParseFS(fs, path)
65 }
66 }
67 index, err := parse("dodo-app-tmpl/index.html")
68 if err != nil {
69 return dodoAppTmplts{}, err
70 }
71 appStatus, err := parse("dodo-app-tmpl/app_status.html")
72 if err != nil {
73 return dodoAppTmplts{}, err
74 }
75 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040076}
77
gio0eaf2712024-04-14 13:08:46 +040078type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040079 l sync.Locker
80 st Store
gio11617ac2024-07-15 16:09:04 +040081 nf NetworkFilter
82 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040083 port int
84 apiPort int
85 self string
gio11617ac2024-07-15 16:09:04 +040086 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040087 sshKey string
88 gitRepoPublicKey string
89 client soft.Client
90 namespace string
91 envAppManagerAddr string
92 env installer.EnvConfig
93 nsc installer.NamespaceCreator
94 jc installer.JobCreator
95 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040096 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +040097 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040098 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +040099 external bool
100 fetchUsersAddr string
giod8ab4f52024-07-26 16:58:34 +0400101}
102
103type appConfig struct {
104 Namespace string `json:"namespace"`
105 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400106}
107
gio33059762024-07-05 13:19:07 +0400108// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400109func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400110 st Store,
gio11617ac2024-07-15 16:09:04 +0400111 nf NetworkFilter,
112 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400113 port int,
gioa60f0de2024-07-08 10:49:48 +0400114 apiPort int,
gio33059762024-07-05 13:19:07 +0400115 self string,
gio11617ac2024-07-15 16:09:04 +0400116 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400117 sshKey string,
gio33059762024-07-05 13:19:07 +0400118 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400119 client soft.Client,
120 namespace string,
giocb34ad22024-07-11 08:01:13 +0400121 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400122 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400123 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400124 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400125 external bool,
126 fetchUsersAddr string,
gio9d66f322024-07-06 13:45:10 +0400127) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400128 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
129 if err != nil {
130 return nil, err
131 }
gio5e49bb62024-07-20 10:43:19 +0400132 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
133 if err != nil {
134 return nil, err
135 }
136 appTmpls, err := NewAppTmplStoreFS(apps)
137 if err != nil {
138 return nil, err
139 }
gio9d66f322024-07-06 13:45:10 +0400140 s := &DodoAppServer{
141 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400142 st,
gio11617ac2024-07-15 16:09:04 +0400143 nf,
144 ug,
gio0eaf2712024-04-14 13:08:46 +0400145 port,
gioa60f0de2024-07-08 10:49:48 +0400146 apiPort,
gio33059762024-07-05 13:19:07 +0400147 self,
gio11617ac2024-07-15 16:09:04 +0400148 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400149 sshKey,
gio33059762024-07-05 13:19:07 +0400150 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400151 client,
152 namespace,
giocb34ad22024-07-11 08:01:13 +0400153 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400154 env,
gio33059762024-07-05 13:19:07 +0400155 nsc,
giof8843412024-05-22 16:38:05 +0400156 jc,
gio266c04f2024-07-03 14:18:45 +0400157 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400158 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400159 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400160 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400161 external,
162 fetchUsersAddr,
gio0eaf2712024-04-14 13:08:46 +0400163 }
gioa60f0de2024-07-08 10:49:48 +0400164 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400165 if err != nil {
166 return nil, err
167 }
giod8ab4f52024-07-26 16:58:34 +0400168 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400169 if err == nil {
170 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400171 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400172 return nil, err
173 }
174 } else if !errors.Is(err, fs.ErrNotExist) {
175 return nil, err
176 }
177 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400178}
179
180func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400181 e := make(chan error)
182 go func() {
183 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400184 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400185 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400186 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400187 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
188 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400189 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
190 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
191 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
192 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400193 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400194 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
195 }()
196 go func() {
197 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400198 r.HandleFunc("/update", s.handleAPIUpdate)
199 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
200 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
giocafd4e62024-07-31 10:53:40 +0400201 if !s.external {
202 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
203 }
gioa60f0de2024-07-08 10:49:48 +0400204 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
205 }()
giocafd4e62024-07-31 10:53:40 +0400206 if !s.external {
207 go func() {
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400208 rand.Seed(uint64(time.Now().UnixNano()))
giocafd4e62024-07-31 10:53:40 +0400209 s.syncUsers()
210 // TODO(dtabidze): every sync delay should be randomized to avoid all client
211 // applications hitting memberships service at the same time.
212 // For every next sync new delay should be randomly generated from scratch.
213 // We can choose random delay from 1 to 2 minutes.
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400214 // for range time.Tick(1 * time.Minute) {
215 // s.syncUsers()
216 // }
217 for {
218 delay := time.Duration(rand.Intn(60)+60) * time.Second
219 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400220 s.syncUsers()
221 }
222 }()
223 }
gioa60f0de2024-07-08 10:49:48 +0400224 return <-e
225}
226
gio11617ac2024-07-15 16:09:04 +0400227type UserGetter interface {
228 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400229 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400230}
231
232type externalUserGetter struct {
233 sc *securecookie.SecureCookie
234}
235
236func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400237 return &externalUserGetter{securecookie.New(
238 securecookie.GenerateRandomKey(64),
239 securecookie.GenerateRandomKey(32),
240 )}
gio11617ac2024-07-15 16:09:04 +0400241}
242
243func (ug *externalUserGetter) Get(r *http.Request) string {
244 cookie, err := r.Cookie(sessionCookie)
245 if err != nil {
246 return ""
247 }
248 var user string
249 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
250 return ""
251 }
252 return user
253}
254
gio8fae3af2024-07-25 13:43:31 +0400255func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
256 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
257 cookie := &http.Cookie{
258 Name: sessionCookie,
259 Value: encoded,
260 Path: "/",
261 Secure: true,
262 HttpOnly: true,
263 }
264 http.SetCookie(w, cookie)
265 return nil
266 } else {
267 return err
268 }
269}
270
gio11617ac2024-07-15 16:09:04 +0400271type internalUserGetter struct{}
272
273func NewInternalUserGetter() UserGetter {
274 return internalUserGetter{}
275}
276
277func (ug internalUserGetter) Get(r *http.Request) string {
278 return r.Header.Get("X-User")
279}
280
gio8fae3af2024-07-25 13:43:31 +0400281func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
282 return nil
283}
284
gio81246f02024-07-10 12:02:15 +0400285func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
286 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400287 if strings.HasSuffix(r.URL.Path, loginPath) ||
288 strings.HasPrefix(r.URL.Path, logoutPath) ||
289 strings.HasPrefix(r.URL.Path, staticPath) ||
290 strings.HasPrefix(r.URL.Path, apiPublicData) ||
291 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400292 next.ServeHTTP(w, r)
293 return
294 }
gio11617ac2024-07-15 16:09:04 +0400295 user := s.ug.Get(r)
296 if user == "" {
gio81246f02024-07-10 12:02:15 +0400297 vars := mux.Vars(r)
298 appName, ok := vars["app-name"]
299 if !ok || appName == "" {
300 http.Error(w, "missing app-name", http.StatusBadRequest)
301 return
302 }
303 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
304 return
305 }
gio81246f02024-07-10 12:02:15 +0400306 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
307 })
308}
309
310func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400311 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400312 http.SetCookie(w, &http.Cookie{
313 Name: sessionCookie,
314 Value: "",
315 Path: "/",
316 HttpOnly: true,
317 Secure: true,
318 })
319 http.Redirect(w, r, "/", http.StatusSeeOther)
320}
321
322func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
323 vars := mux.Vars(r)
324 appName, ok := vars["app-name"]
325 if !ok || appName == "" {
326 http.Error(w, "missing app-name", http.StatusBadRequest)
327 return
328 }
329 fmt.Fprint(w, `
330<!DOCTYPE html>
331<html lang='en'>
332 <head>
333 <title>dodo: app - login</title>
334 <meta charset='utf-8'>
335 </head>
336 <body>
337 <form action="" method="POST">
338 <input type="password" placeholder="Password" name="password" required />
339 <button type="submit">Login</button>
340 </form>
341 </body>
342</html>
343`)
344}
345
346func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
347 vars := mux.Vars(r)
348 appName, ok := vars["app-name"]
349 if !ok || appName == "" {
350 http.Error(w, "missing app-name", http.StatusBadRequest)
351 return
352 }
353 password := r.FormValue("password")
354 if password == "" {
355 http.Error(w, "missing password", http.StatusBadRequest)
356 return
357 }
358 user, err := s.st.GetAppOwner(appName)
359 if err != nil {
360 http.Error(w, err.Error(), http.StatusInternalServerError)
361 return
362 }
363 hashed, err := s.st.GetUserPassword(user)
364 if err != nil {
365 http.Error(w, err.Error(), http.StatusInternalServerError)
366 return
367 }
368 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
369 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
370 return
371 }
gio8fae3af2024-07-25 13:43:31 +0400372 if err := s.ug.Encode(w, user); err != nil {
373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 return
gio81246f02024-07-10 12:02:15 +0400375 }
376 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
377}
378
gio23bdc1b2024-07-11 16:07:47 +0400379type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400380 Apps []string
381 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400382 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400383}
384
gioa60f0de2024-07-08 10:49:48 +0400385func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400386 user := r.Context().Value(userCtx)
387 if user == nil {
388 http.Error(w, "unauthorized", http.StatusUnauthorized)
389 return
390 }
391 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400392 if err != nil {
393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
395 }
gio11617ac2024-07-15 16:09:04 +0400396 networks, err := s.getNetworks(user.(string))
397 if err != nil {
398 http.Error(w, err.Error(), http.StatusInternalServerError)
399 return
400 }
giob54db242024-07-30 18:49:33 +0400401 var types []string
402 for _, t := range s.appTmpls.Types() {
403 types = append(types, strings.Replace(t, "-", ":", 1))
404 }
gio5e49bb62024-07-20 10:43:19 +0400405 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400406 if err := s.tmplts.index.Execute(w, data); err != nil {
407 http.Error(w, err.Error(), http.StatusInternalServerError)
408 return
gioa60f0de2024-07-08 10:49:48 +0400409 }
410}
411
gio5e49bb62024-07-20 10:43:19 +0400412type appStatusData struct {
413 Name string
414 GitCloneCommand string
415 Commits []Commit
416}
417
gioa60f0de2024-07-08 10:49:48 +0400418func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
419 vars := mux.Vars(r)
420 appName, ok := vars["app-name"]
421 if !ok || appName == "" {
422 http.Error(w, "missing app-name", http.StatusBadRequest)
423 return
424 }
gio94904702024-07-26 16:58:34 +0400425 u := r.Context().Value(userCtx)
426 if u == nil {
427 http.Error(w, "unauthorized", http.StatusUnauthorized)
428 return
429 }
430 user, ok := u.(string)
431 if !ok {
432 http.Error(w, "could not get user", http.StatusInternalServerError)
433 return
434 }
435 owner, err := s.st.GetAppOwner(appName)
436 if err != nil {
437 http.Error(w, err.Error(), http.StatusInternalServerError)
438 return
439 }
440 if owner != user {
441 http.Error(w, "unauthorized", http.StatusUnauthorized)
442 return
443 }
gioa60f0de2024-07-08 10:49:48 +0400444 commits, err := s.st.GetCommitHistory(appName)
445 if err != nil {
446 http.Error(w, err.Error(), http.StatusInternalServerError)
447 return
448 }
gio5e49bb62024-07-20 10:43:19 +0400449 data := appStatusData{
450 Name: appName,
451 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
452 Commits: commits,
453 }
454 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
455 http.Error(w, err.Error(), http.StatusInternalServerError)
456 return
gioa60f0de2024-07-08 10:49:48 +0400457 }
gio0eaf2712024-04-14 13:08:46 +0400458}
459
gio81246f02024-07-10 12:02:15 +0400460type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400461 Ref string `json:"ref"`
462 Repository struct {
463 Name string `json:"name"`
464 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400465 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400466}
467
gio8fae3af2024-07-25 13:43:31 +0400468func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400469 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400470 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400471 var contents strings.Builder
472 io.Copy(&contents, r.Body)
473 c := contents.String()
474 fmt.Println(c)
475 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400476 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400477 return
478 }
gioa60f0de2024-07-08 10:49:48 +0400479 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400480 return
481 }
gioa60f0de2024-07-08 10:49:48 +0400482 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400483 go func() {
gio11617ac2024-07-15 16:09:04 +0400484 owner, err := s.st.GetAppOwner(req.Repository.Name)
485 if err != nil {
486 return
487 }
488 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400489 if err != nil {
490 return
491 }
gio94904702024-07-26 16:58:34 +0400492 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
493 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
494 if err != nil {
495 return
496 }
497 if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400498 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
499 fmt.Printf("Error: %s\n", err.Error())
500 return
501 }
502 }
503 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
504 fmt.Printf("Error: %s\n", err.Error())
505 }
506 for addr, _ := range s.workers[req.Repository.Name] {
507 go func() {
508 // TODO(gio): make port configurable
509 http.Get(fmt.Sprintf("http://%s/update", addr))
510 }()
gio0eaf2712024-04-14 13:08:46 +0400511 }
512 }()
gio0eaf2712024-04-14 13:08:46 +0400513}
514
gio81246f02024-07-10 12:02:15 +0400515type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400516 Address string `json:"address"`
517}
518
gio8fae3af2024-07-25 13:43:31 +0400519func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400520 vars := mux.Vars(r)
521 appName, ok := vars["app-name"]
522 if !ok || appName == "" {
523 http.Error(w, "missing app-name", http.StatusBadRequest)
524 return
525 }
gio81246f02024-07-10 12:02:15 +0400526 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400527 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
528 http.Error(w, err.Error(), http.StatusInternalServerError)
529 return
530 }
gioa60f0de2024-07-08 10:49:48 +0400531 if _, ok := s.workers[appName]; !ok {
532 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400533 }
gioa60f0de2024-07-08 10:49:48 +0400534 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400535}
536
gio11617ac2024-07-15 16:09:04 +0400537func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
538 u := r.Context().Value(userCtx)
539 if u == nil {
540 http.Error(w, "unauthorized", http.StatusUnauthorized)
541 return
542 }
543 user, ok := u.(string)
544 if !ok {
545 http.Error(w, "could not get user", http.StatusInternalServerError)
546 return
547 }
548 network := r.FormValue("network")
549 if network == "" {
550 http.Error(w, "missing network", http.StatusBadRequest)
551 return
552 }
gio5e49bb62024-07-20 10:43:19 +0400553 subdomain := r.FormValue("subdomain")
554 if subdomain == "" {
555 http.Error(w, "missing subdomain", http.StatusBadRequest)
556 return
557 }
558 appType := r.FormValue("type")
559 if appType == "" {
560 http.Error(w, "missing type", http.StatusBadRequest)
561 return
562 }
gio11617ac2024-07-15 16:09:04 +0400563 g := installer.NewFixedLengthRandomNameGenerator(3)
564 appName, err := g.Generate()
565 if err != nil {
566 http.Error(w, err.Error(), http.StatusInternalServerError)
567 return
568 }
569 if ok, err := s.client.UserExists(user); err != nil {
570 http.Error(w, err.Error(), http.StatusInternalServerError)
571 return
572 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400573 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
574 return
gio11617ac2024-07-15 16:09:04 +0400575 }
giocafd4e62024-07-31 10:53:40 +0400576 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580 if err := s.st.CreateApp(appName, user); err != nil {
581 http.Error(w, err.Error(), http.StatusInternalServerError)
582 return
583 }
giod8ab4f52024-07-26 16:58:34 +0400584 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400585 http.Error(w, err.Error(), http.StatusInternalServerError)
586 return
587 }
588 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
589}
590
gio81246f02024-07-10 12:02:15 +0400591type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400592 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400593 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400594 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400595 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400596}
597
gio81246f02024-07-10 12:02:15 +0400598type apiCreateAppResp struct {
599 AppName string `json:"appName"`
600 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400601}
602
gio8fae3af2024-07-25 13:43:31 +0400603func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400604 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400605 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400606 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
607 http.Error(w, err.Error(), http.StatusBadRequest)
608 return
609 }
610 g := installer.NewFixedLengthRandomNameGenerator(3)
611 appName, err := g.Generate()
612 if err != nil {
613 http.Error(w, err.Error(), http.StatusInternalServerError)
614 return
615 }
gio11617ac2024-07-15 16:09:04 +0400616 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400617 if err != nil {
gio33059762024-07-05 13:19:07 +0400618 http.Error(w, err.Error(), http.StatusInternalServerError)
619 return
620 }
gio11617ac2024-07-15 16:09:04 +0400621 if user != "" {
622 http.Error(w, "public key already registered", http.StatusBadRequest)
623 return
624 }
625 user = appName
626 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
627 http.Error(w, err.Error(), http.StatusInternalServerError)
628 return
629 }
630 password := generatePassword()
631 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
632 if err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
giocafd4e62024-07-31 10:53:40 +0400636 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400637 http.Error(w, err.Error(), http.StatusInternalServerError)
638 return
639 }
640 if err := s.st.CreateApp(appName, user); err != nil {
641 http.Error(w, err.Error(), http.StatusInternalServerError)
642 return
643 }
giod8ab4f52024-07-26 16:58:34 +0400644 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400645 http.Error(w, err.Error(), http.StatusInternalServerError)
646 return
647 }
gio81246f02024-07-10 12:02:15 +0400648 resp := apiCreateAppResp{
649 AppName: appName,
650 Password: password,
651 }
gio33059762024-07-05 13:19:07 +0400652 if err := json.NewEncoder(w).Encode(resp); err != nil {
653 http.Error(w, err.Error(), http.StatusInternalServerError)
654 return
655 }
656}
657
giod8ab4f52024-07-26 16:58:34 +0400658func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400659 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400660 return true
661 }
662 for _, cfg := range s.appConfigs {
663 if strings.ToLower(cfg.Network) == network {
664 return false
665 }
666 }
667 return true
668}
669
670func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400671 s.l.Lock()
672 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400673 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400674 network = strings.ToLower(network)
675 if !s.isNetworkUseAllowed(network) {
676 return fmt.Errorf("network already used: %s", network)
677 }
gio33059762024-07-05 13:19:07 +0400678 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400679 return err
gio33059762024-07-05 13:19:07 +0400680 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400681 return nil
gioa60f0de2024-07-08 10:49:48 +0400682 }
gio5e49bb62024-07-20 10:43:19 +0400683 networks, err := s.getNetworks(user)
684 if err != nil {
685 return err
686 }
giod8ab4f52024-07-26 16:58:34 +0400687 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400688 if !ok {
689 return fmt.Errorf("network not found: %s\n", network)
690 }
gio33059762024-07-05 13:19:07 +0400691 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400692 return err
gio33059762024-07-05 13:19:07 +0400693 }
694 appRepo, err := s.client.GetRepo(appName)
695 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400696 return err
gio33059762024-07-05 13:19:07 +0400697 }
gio5e49bb62024-07-20 10:43:19 +0400698 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400699 return err
gio33059762024-07-05 13:19:07 +0400700 }
701 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400702 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
703 if err != nil {
704 return err
705 }
706 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400707 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400708 return err
gio33059762024-07-05 13:19:07 +0400709 }
710 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
711 suffix, err := suffixGen.Generate()
712 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400713 return err
gio33059762024-07-05 13:19:07 +0400714 }
gio94904702024-07-26 16:58:34 +0400715 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400716 s.appConfigs[appName] = appConfig{namespace, network}
gio94904702024-07-26 16:58:34 +0400717 if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400718 return err
gio33059762024-07-05 13:19:07 +0400719 }
giod8ab4f52024-07-26 16:58:34 +0400720 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400721 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400722 return err
gio33059762024-07-05 13:19:07 +0400723 }
724 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400725 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400726 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400727 return err
gio33059762024-07-05 13:19:07 +0400728 }
giod8ab4f52024-07-26 16:58:34 +0400729 if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
730 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400731 if err != nil {
732 return "", err
733 }
734 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400735 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400736 return "", err
737 }
738 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400739 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400740 appName,
741 "/"+appName,
742 namespace,
743 map[string]any{
744 "repoAddr": s.client.GetRepoAddress(appName),
745 "repoHost": strings.Split(s.client.Address(), ":")[0],
746 "gitRepoPublicKey": s.gitRepoPublicKey,
747 },
748 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400749 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400750 installer.WithNoPublish(),
751 installer.WithNoLock(),
752 ); err != nil {
753 return "", err
754 }
755 return fmt.Sprintf("Installed app: %s", appName), nil
756 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400757 return err
gio33059762024-07-05 13:19:07 +0400758 }
759 cfg, err := m.FindInstance(appName)
760 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400761 return err
gio33059762024-07-05 13:19:07 +0400762 }
763 fluxKeys, ok := cfg.Input["fluxKeys"]
764 if !ok {
gio11617ac2024-07-15 16:09:04 +0400765 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400766 }
767 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
768 if !ok {
gio11617ac2024-07-15 16:09:04 +0400769 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400770 }
771 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400772 return err
gio33059762024-07-05 13:19:07 +0400773 } else if ok {
774 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400775 return err
gio33059762024-07-05 13:19:07 +0400776 }
777 } else {
778 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400779 return err
gio33059762024-07-05 13:19:07 +0400780 }
781 }
782 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400783 return err
gio33059762024-07-05 13:19:07 +0400784 }
785 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 +0400786 return err
gio33059762024-07-05 13:19:07 +0400787 }
gio81246f02024-07-10 12:02:15 +0400788 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400789 return err
gio33059762024-07-05 13:19:07 +0400790 }
gio2ccb6e32024-08-15 12:01:33 +0400791 if !s.external {
792 go func() {
793 users, err := s.client.GetAllUsers()
794 if err != nil {
795 fmt.Println(err)
796 return
797 }
798 for _, user := range users {
799 // TODO(gio): fluxcd should have only read access
800 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
801 fmt.Println(err)
802 }
803 }
804 }()
805 }
gio11617ac2024-07-15 16:09:04 +0400806 return nil
gio33059762024-07-05 13:19:07 +0400807}
808
gio81246f02024-07-10 12:02:15 +0400809type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400810 Public string `json:"public"`
811}
812
gio8fae3af2024-07-25 13:43:31 +0400813func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400814 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400815 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
816 http.Error(w, err.Error(), http.StatusBadRequest)
817 return
818 }
819 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
820 http.Error(w, err.Error(), http.StatusInternalServerError)
821 return
822 }
823}
824
gio94904702024-07-26 16:58:34 +0400825type dodoAppRendered struct {
826 App struct {
827 Ingress struct {
828 Network string `json:"network"`
829 Subdomain string `json:"subdomain"`
830 } `json:"ingress"`
831 } `json:"app"`
832 Input struct {
833 AppId string `json:"appId"`
834 } `json:"input"`
835}
836
837func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400838 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400839 if err != nil {
840 return err
841 }
giof8843412024-05-22 16:38:05 +0400842 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400843 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400844 if err != nil {
845 return err
846 }
847 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400848 if err != nil {
849 return err
850 }
851 app, err := installer.NewDodoApp(appCfg)
852 if err != nil {
853 return err
854 }
giof8843412024-05-22 16:38:05 +0400855 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
gio94904702024-07-26 16:58:34 +0400856 return repo.Do(func(r soft.RepoFS) (string, error) {
857 res, err := m.Install(
858 app,
859 "app",
860 "/.dodo/app",
861 namespace,
862 map[string]any{
863 "repoAddr": repo.FullAddress(),
864 "managerAddr": fmt.Sprintf("http://%s", s.self),
865 "appId": name,
866 "sshPrivateKey": s.sshKey,
867 },
868 installer.WithNoPull(),
869 installer.WithNoPublish(),
870 installer.WithConfig(&s.env),
871 installer.WithNetworks(networks),
872 installer.WithLocalChartGenerator(lg),
873 installer.WithNoLock(),
874 )
875 if err != nil {
876 return "", err
877 }
878 var rendered dodoAppRendered
879 if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
880 return "", nil
881 }
882 if _, err := m.Install(
883 appStatus,
884 "status",
885 "/.dodo/status",
886 s.namespace,
887 map[string]any{
888 "appName": rendered.Input.AppId,
889 "network": rendered.App.Ingress.Network,
890 "appSubdomain": rendered.App.Ingress.Subdomain,
891 },
892 installer.WithNoPull(),
893 installer.WithNoPublish(),
894 installer.WithConfig(&s.env),
895 installer.WithNetworks(networks),
896 installer.WithLocalChartGenerator(lg),
897 installer.WithNoLock(),
898 ); err != nil {
899 return "", err
900 }
901 return "install app", nil
902 },
903 soft.WithCommitToBranch("dodo"),
904 soft.WithForce(),
905 )
gio0eaf2712024-04-14 13:08:46 +0400906}
gio33059762024-07-05 13:19:07 +0400907
gio5e49bb62024-07-20 10:43:19 +0400908func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
giob54db242024-07-30 18:49:33 +0400909 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +0400910 appTmpl, err := s.appTmpls.Find(appType)
911 if err != nil {
912 return err
gio33059762024-07-05 13:19:07 +0400913 }
gio33059762024-07-05 13:19:07 +0400914 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400915 if err := appTmpl.Render(network, subdomain, repo); err != nil {
916 return "", err
gio33059762024-07-05 13:19:07 +0400917 }
gio5e49bb62024-07-20 10:43:19 +0400918 return "init", nil
gio33059762024-07-05 13:19:07 +0400919 })
920}
gio81246f02024-07-10 12:02:15 +0400921
922func generatePassword() string {
923 return "foo"
924}
giocb34ad22024-07-11 08:01:13 +0400925
gio11617ac2024-07-15 16:09:04 +0400926func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400927 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400928 resp, err := http.Get(addr)
929 if err != nil {
930 return nil, err
931 }
gio23bdc1b2024-07-11 16:07:47 +0400932 networks := []installer.Network{}
933 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400934 return nil, err
935 }
gio11617ac2024-07-15 16:09:04 +0400936 return s.nf.Filter(user, networks)
937}
938
gio8fae3af2024-07-25 13:43:31 +0400939type publicNetworkData struct {
940 Name string `json:"name"`
941 Domain string `json:"domain"`
942}
943
944type publicData struct {
945 Networks []publicNetworkData `json:"networks"`
946 Types []string `json:"types"`
947}
948
949func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400950 w.Header().Set("Access-Control-Allow-Origin", "*")
951 s.l.Lock()
952 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +0400953 networks, err := s.getNetworks("")
954 if err != nil {
955 http.Error(w, err.Error(), http.StatusInternalServerError)
956 return
957 }
958 var ret publicData
959 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +0400960 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
961 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
962 }
gio8fae3af2024-07-25 13:43:31 +0400963 }
964 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +0400965 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +0400966 }
gio8fae3af2024-07-25 13:43:31 +0400967 if err := json.NewEncoder(w).Encode(ret); err != nil {
968 http.Error(w, err.Error(), http.StatusInternalServerError)
969 return
970 }
971}
972
gio11617ac2024-07-15 16:09:04 +0400973func pickNetwork(networks []installer.Network, network string) []installer.Network {
974 for _, n := range networks {
975 if n.Name == network {
976 return []installer.Network{n}
977 }
978 }
979 return []installer.Network{}
980}
981
982type NetworkFilter interface {
983 Filter(user string, networks []installer.Network) ([]installer.Network, error)
984}
985
986type noNetworkFilter struct{}
987
988func NewNoNetworkFilter() NetworkFilter {
989 return noNetworkFilter{}
990}
991
gio8fae3af2024-07-25 13:43:31 +0400992func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400993 return networks, nil
994}
995
996type filterByOwner struct {
997 st Store
998}
999
1000func NewNetworkFilterByOwner(st Store) NetworkFilter {
1001 return &filterByOwner{st}
1002}
1003
1004func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001005 if user == "" {
1006 return networks, nil
1007 }
gio11617ac2024-07-15 16:09:04 +04001008 network, err := f.st.GetUserNetwork(user)
1009 if err != nil {
1010 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001011 }
1012 ret := []installer.Network{}
1013 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001014 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001015 ret = append(ret, n)
1016 }
1017 }
giocb34ad22024-07-11 08:01:13 +04001018 return ret, nil
1019}
gio11617ac2024-07-15 16:09:04 +04001020
1021type allowListFilter struct {
1022 allowed []string
1023}
1024
1025func NewAllowListFilter(allowed []string) NetworkFilter {
1026 return &allowListFilter{allowed}
1027}
1028
gio8fae3af2024-07-25 13:43:31 +04001029func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001030 ret := []installer.Network{}
1031 for _, n := range networks {
1032 if slices.Contains(f.allowed, n.Name) {
1033 ret = append(ret, n)
1034 }
1035 }
1036 return ret, nil
1037}
1038
1039type combinedNetworkFilter struct {
1040 filters []NetworkFilter
1041}
1042
1043func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1044 return &combinedNetworkFilter{filters}
1045}
1046
gio8fae3af2024-07-25 13:43:31 +04001047func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001048 ret := networks
1049 var err error
1050 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001051 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001052 if err != nil {
1053 return nil, err
1054 }
1055 }
1056 return ret, nil
1057}
giocafd4e62024-07-31 10:53:40 +04001058
1059type user struct {
1060 Username string `json:"username"`
1061 Email string `json:"email"`
1062 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1063}
1064
1065func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1066 go s.syncUsers()
1067}
1068
1069func (s *DodoAppServer) syncUsers() {
1070 if s.external {
1071 panic("MUST NOT REACH!")
1072 }
1073 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1074 if err != nil {
1075 return
1076 }
1077 users := []user{}
1078 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1079 fmt.Println(err)
1080 return
1081 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001082 validUsernames := make(map[string]user)
1083 for _, u := range users {
1084 validUsernames[u.Username] = u
1085 }
1086 allClientUsers, err := s.client.GetAllUsers()
1087 if err != nil {
1088 fmt.Println(err)
1089 return
1090 }
1091 keyToUser := make(map[string]string)
1092 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001093 if clientUser == "admin" || clientUser == "fluxcd" {
1094 continue
1095 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001096 userData, ok := validUsernames[clientUser]
1097 if !ok {
1098 if err := s.client.RemoveUser(clientUser); err != nil {
1099 fmt.Println(err)
1100 return
1101 }
1102 } else {
1103 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1104 if err != nil {
1105 fmt.Println(err)
1106 return
1107 }
1108 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001109 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001110 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001111 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001112 })
1113 if !keyOk {
1114 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1115 fmt.Println(err)
1116 }
1117 } else {
1118 keyToUser[cleanKey] = clientUser
1119 }
1120 }
1121 }
1122 }
giocafd4e62024-07-31 10:53:40 +04001123 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001124 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1125 fmt.Println(err)
1126 return
1127 }
giocafd4e62024-07-31 10:53:40 +04001128 if len(u.SSHPublicKeys) == 0 {
1129 continue
1130 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001131 ok, err := s.client.UserExists(u.Username)
1132 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001133 fmt.Println(err)
1134 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001135 }
1136 if !ok {
1137 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1138 fmt.Println(err)
1139 return
1140 }
1141 } else {
1142 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001143 cleanKey := soft.CleanKey(key)
1144 if user, ok := keyToUser[cleanKey]; ok {
1145 if u.Username != user {
1146 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1147 }
1148 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001149 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001150 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001151 fmt.Println(err)
1152 return
giocafd4e62024-07-31 10:53:40 +04001153 }
1154 }
1155 }
1156 }
1157 repos, err := s.client.GetAllRepos()
1158 if err != nil {
1159 return
1160 }
1161 for _, r := range repos {
1162 if r == ConfigRepoName {
1163 continue
1164 }
1165 for _, u := range users {
1166 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1167 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001168 continue
giocafd4e62024-07-31 10:53:40 +04001169 }
1170 }
1171 }
1172}