blob: 488047cd1f3357816edc1ebb7a147d104a25e44e [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"
gio81246f02024-07-10 12:02:15 +040010 "golang.org/x/crypto/bcrypt"
gio23bdc1b2024-07-11 16:07:47 +040011 "html/template"
gio0eaf2712024-04-14 13:08:46 +040012 "io"
gio9d66f322024-07-06 13:45:10 +040013 "io/fs"
gio0eaf2712024-04-14 13:08:46 +040014 "net/http"
gio23bdc1b2024-07-11 16:07:47 +040015 "slices"
gio0eaf2712024-04-14 13:08:46 +040016 "strings"
gio9d66f322024-07-06 13:45:10 +040017 "sync"
giocafd4e62024-07-31 10:53:40 +040018 "time"
gio0eaf2712024-04-14 13:08:46 +040019
20 "github.com/giolekva/pcloud/core/installer"
21 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040022
23 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040024 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040025)
26
gio23bdc1b2024-07-11 16:07:47 +040027//go:embed dodo-app-tmpl/*
28var dodoAppTmplFS embed.FS
29
gio5e49bb62024-07-20 10:43:19 +040030//go:embed all:app-tmpl
31var appTmplsFS embed.FS
32
33//go:embed static
34var staticResources embed.FS
35
gio9d66f322024-07-06 13:45:10 +040036const (
gioa60f0de2024-07-08 10:49:48 +040037 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040038 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040039 loginPath = "/login"
40 logoutPath = "/logout"
gio5e49bb62024-07-20 10:43:19 +040041 staticPath = "/static"
gio8fae3af2024-07-25 13:43:31 +040042 apiPublicData = "/api/public-data"
43 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040044 sessionCookie = "dodo-app-session"
45 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040046)
47
gio23bdc1b2024-07-11 16:07:47 +040048type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040049 index *template.Template
50 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040051}
52
53func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040054 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040055 if err != nil {
56 return dodoAppTmplts{}, err
57 }
gio5e49bb62024-07-20 10:43:19 +040058 parse := func(path string) (*template.Template, error) {
59 if b, err := base.Clone(); err != nil {
60 return nil, err
61 } else {
62 return b.ParseFS(fs, path)
63 }
64 }
65 index, err := parse("dodo-app-tmpl/index.html")
66 if err != nil {
67 return dodoAppTmplts{}, err
68 }
69 appStatus, err := parse("dodo-app-tmpl/app_status.html")
70 if err != nil {
71 return dodoAppTmplts{}, err
72 }
73 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040074}
75
gio0eaf2712024-04-14 13:08:46 +040076type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040077 l sync.Locker
78 st Store
gio11617ac2024-07-15 16:09:04 +040079 nf NetworkFilter
80 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040081 port int
82 apiPort int
83 self string
gio11617ac2024-07-15 16:09:04 +040084 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040085 sshKey string
86 gitRepoPublicKey string
87 client soft.Client
88 namespace string
89 envAppManagerAddr string
90 env installer.EnvConfig
91 nsc installer.NamespaceCreator
92 jc installer.JobCreator
93 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040094 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +040095 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040096 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +040097 external bool
98 fetchUsersAddr string
giod8ab4f52024-07-26 16:58:34 +040099}
100
101type appConfig struct {
102 Namespace string `json:"namespace"`
103 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400104}
105
gio33059762024-07-05 13:19:07 +0400106// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400107func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400108 st Store,
gio11617ac2024-07-15 16:09:04 +0400109 nf NetworkFilter,
110 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400111 port int,
gioa60f0de2024-07-08 10:49:48 +0400112 apiPort int,
gio33059762024-07-05 13:19:07 +0400113 self string,
gio11617ac2024-07-15 16:09:04 +0400114 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400115 sshKey string,
gio33059762024-07-05 13:19:07 +0400116 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400117 client soft.Client,
118 namespace string,
giocb34ad22024-07-11 08:01:13 +0400119 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400120 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400121 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400122 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400123 external bool,
124 fetchUsersAddr string,
gio9d66f322024-07-06 13:45:10 +0400125) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400126 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
127 if err != nil {
128 return nil, err
129 }
gio5e49bb62024-07-20 10:43:19 +0400130 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
131 if err != nil {
132 return nil, err
133 }
134 appTmpls, err := NewAppTmplStoreFS(apps)
135 if err != nil {
136 return nil, err
137 }
gio9d66f322024-07-06 13:45:10 +0400138 s := &DodoAppServer{
139 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400140 st,
gio11617ac2024-07-15 16:09:04 +0400141 nf,
142 ug,
gio0eaf2712024-04-14 13:08:46 +0400143 port,
gioa60f0de2024-07-08 10:49:48 +0400144 apiPort,
gio33059762024-07-05 13:19:07 +0400145 self,
gio11617ac2024-07-15 16:09:04 +0400146 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400147 sshKey,
gio33059762024-07-05 13:19:07 +0400148 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400149 client,
150 namespace,
giocb34ad22024-07-11 08:01:13 +0400151 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400152 env,
gio33059762024-07-05 13:19:07 +0400153 nsc,
giof8843412024-05-22 16:38:05 +0400154 jc,
gio266c04f2024-07-03 14:18:45 +0400155 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400156 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400157 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400158 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400159 external,
160 fetchUsersAddr,
gio0eaf2712024-04-14 13:08:46 +0400161 }
gioa60f0de2024-07-08 10:49:48 +0400162 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400163 if err != nil {
164 return nil, err
165 }
giod8ab4f52024-07-26 16:58:34 +0400166 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400167 if err == nil {
168 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400169 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400170 return nil, err
171 }
172 } else if !errors.Is(err, fs.ErrNotExist) {
173 return nil, err
174 }
175 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400176}
177
178func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400179 e := make(chan error)
180 go func() {
181 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400182 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400183 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400184 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400185 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
186 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400187 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
188 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
189 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
190 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400191 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400192 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
193 }()
194 go func() {
195 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400196 r.HandleFunc("/update", s.handleAPIUpdate)
197 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
198 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
giocafd4e62024-07-31 10:53:40 +0400199 if !s.external {
200 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
201 }
gioa60f0de2024-07-08 10:49:48 +0400202 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
203 }()
giocafd4e62024-07-31 10:53:40 +0400204 if !s.external {
205 go func() {
206 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.
211 for range time.Tick(1 * time.Minute) {
212 s.syncUsers()
213 }
214 }()
215 }
gioa60f0de2024-07-08 10:49:48 +0400216 return <-e
217}
218
gio11617ac2024-07-15 16:09:04 +0400219type UserGetter interface {
220 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400221 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400222}
223
224type externalUserGetter struct {
225 sc *securecookie.SecureCookie
226}
227
228func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400229 return &externalUserGetter{securecookie.New(
230 securecookie.GenerateRandomKey(64),
231 securecookie.GenerateRandomKey(32),
232 )}
gio11617ac2024-07-15 16:09:04 +0400233}
234
235func (ug *externalUserGetter) Get(r *http.Request) string {
236 cookie, err := r.Cookie(sessionCookie)
237 if err != nil {
238 return ""
239 }
240 var user string
241 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
242 return ""
243 }
244 return user
245}
246
gio8fae3af2024-07-25 13:43:31 +0400247func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
248 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
249 cookie := &http.Cookie{
250 Name: sessionCookie,
251 Value: encoded,
252 Path: "/",
253 Secure: true,
254 HttpOnly: true,
255 }
256 http.SetCookie(w, cookie)
257 return nil
258 } else {
259 return err
260 }
261}
262
gio11617ac2024-07-15 16:09:04 +0400263type internalUserGetter struct{}
264
265func NewInternalUserGetter() UserGetter {
266 return internalUserGetter{}
267}
268
269func (ug internalUserGetter) Get(r *http.Request) string {
270 return r.Header.Get("X-User")
271}
272
gio8fae3af2024-07-25 13:43:31 +0400273func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
274 return nil
275}
276
gio81246f02024-07-10 12:02:15 +0400277func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
278 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400279 if strings.HasSuffix(r.URL.Path, loginPath) ||
280 strings.HasPrefix(r.URL.Path, logoutPath) ||
281 strings.HasPrefix(r.URL.Path, staticPath) ||
282 strings.HasPrefix(r.URL.Path, apiPublicData) ||
283 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400284 next.ServeHTTP(w, r)
285 return
286 }
gio11617ac2024-07-15 16:09:04 +0400287 user := s.ug.Get(r)
288 if user == "" {
gio81246f02024-07-10 12:02:15 +0400289 vars := mux.Vars(r)
290 appName, ok := vars["app-name"]
291 if !ok || appName == "" {
292 http.Error(w, "missing app-name", http.StatusBadRequest)
293 return
294 }
295 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
296 return
297 }
gio81246f02024-07-10 12:02:15 +0400298 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
299 })
300}
301
302func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400303 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400304 http.SetCookie(w, &http.Cookie{
305 Name: sessionCookie,
306 Value: "",
307 Path: "/",
308 HttpOnly: true,
309 Secure: true,
310 })
311 http.Redirect(w, r, "/", http.StatusSeeOther)
312}
313
314func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
315 vars := mux.Vars(r)
316 appName, ok := vars["app-name"]
317 if !ok || appName == "" {
318 http.Error(w, "missing app-name", http.StatusBadRequest)
319 return
320 }
321 fmt.Fprint(w, `
322<!DOCTYPE html>
323<html lang='en'>
324 <head>
325 <title>dodo: app - login</title>
326 <meta charset='utf-8'>
327 </head>
328 <body>
329 <form action="" method="POST">
330 <input type="password" placeholder="Password" name="password" required />
331 <button type="submit">Login</button>
332 </form>
333 </body>
334</html>
335`)
336}
337
338func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
339 vars := mux.Vars(r)
340 appName, ok := vars["app-name"]
341 if !ok || appName == "" {
342 http.Error(w, "missing app-name", http.StatusBadRequest)
343 return
344 }
345 password := r.FormValue("password")
346 if password == "" {
347 http.Error(w, "missing password", http.StatusBadRequest)
348 return
349 }
350 user, err := s.st.GetAppOwner(appName)
351 if err != nil {
352 http.Error(w, err.Error(), http.StatusInternalServerError)
353 return
354 }
355 hashed, err := s.st.GetUserPassword(user)
356 if err != nil {
357 http.Error(w, err.Error(), http.StatusInternalServerError)
358 return
359 }
360 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
361 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
362 return
363 }
gio8fae3af2024-07-25 13:43:31 +0400364 if err := s.ug.Encode(w, user); err != nil {
365 http.Error(w, err.Error(), http.StatusInternalServerError)
366 return
gio81246f02024-07-10 12:02:15 +0400367 }
368 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
369}
370
gio23bdc1b2024-07-11 16:07:47 +0400371type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400372 Apps []string
373 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400374 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400375}
376
gioa60f0de2024-07-08 10:49:48 +0400377func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400378 user := r.Context().Value(userCtx)
379 if user == nil {
380 http.Error(w, "unauthorized", http.StatusUnauthorized)
381 return
382 }
383 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400384 if err != nil {
385 http.Error(w, err.Error(), http.StatusInternalServerError)
386 return
387 }
gio11617ac2024-07-15 16:09:04 +0400388 networks, err := s.getNetworks(user.(string))
389 if err != nil {
390 http.Error(w, err.Error(), http.StatusInternalServerError)
391 return
392 }
giob54db242024-07-30 18:49:33 +0400393 var types []string
394 for _, t := range s.appTmpls.Types() {
395 types = append(types, strings.Replace(t, "-", ":", 1))
396 }
gio5e49bb62024-07-20 10:43:19 +0400397 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400398 if err := s.tmplts.index.Execute(w, data); err != nil {
399 http.Error(w, err.Error(), http.StatusInternalServerError)
400 return
gioa60f0de2024-07-08 10:49:48 +0400401 }
402}
403
gio5e49bb62024-07-20 10:43:19 +0400404type appStatusData struct {
405 Name string
406 GitCloneCommand string
407 Commits []Commit
408}
409
gioa60f0de2024-07-08 10:49:48 +0400410func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
411 vars := mux.Vars(r)
412 appName, ok := vars["app-name"]
413 if !ok || appName == "" {
414 http.Error(w, "missing app-name", http.StatusBadRequest)
415 return
416 }
gio94904702024-07-26 16:58:34 +0400417 u := r.Context().Value(userCtx)
418 if u == nil {
419 http.Error(w, "unauthorized", http.StatusUnauthorized)
420 return
421 }
422 user, ok := u.(string)
423 if !ok {
424 http.Error(w, "could not get user", http.StatusInternalServerError)
425 return
426 }
427 owner, err := s.st.GetAppOwner(appName)
428 if err != nil {
429 http.Error(w, err.Error(), http.StatusInternalServerError)
430 return
431 }
432 if owner != user {
433 http.Error(w, "unauthorized", http.StatusUnauthorized)
434 return
435 }
gioa60f0de2024-07-08 10:49:48 +0400436 commits, err := s.st.GetCommitHistory(appName)
437 if err != nil {
438 http.Error(w, err.Error(), http.StatusInternalServerError)
439 return
440 }
gio5e49bb62024-07-20 10:43:19 +0400441 data := appStatusData{
442 Name: appName,
443 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
444 Commits: commits,
445 }
446 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
447 http.Error(w, err.Error(), http.StatusInternalServerError)
448 return
gioa60f0de2024-07-08 10:49:48 +0400449 }
gio0eaf2712024-04-14 13:08:46 +0400450}
451
gio81246f02024-07-10 12:02:15 +0400452type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400453 Ref string `json:"ref"`
454 Repository struct {
455 Name string `json:"name"`
456 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400457 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400458}
459
gio8fae3af2024-07-25 13:43:31 +0400460func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400461 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400462 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400463 var contents strings.Builder
464 io.Copy(&contents, r.Body)
465 c := contents.String()
466 fmt.Println(c)
467 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400468 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400469 return
470 }
gioa60f0de2024-07-08 10:49:48 +0400471 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400472 return
473 }
gioa60f0de2024-07-08 10:49:48 +0400474 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400475 go func() {
gio11617ac2024-07-15 16:09:04 +0400476 owner, err := s.st.GetAppOwner(req.Repository.Name)
477 if err != nil {
478 return
479 }
480 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400481 if err != nil {
482 return
483 }
gio94904702024-07-26 16:58:34 +0400484 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
485 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
486 if err != nil {
487 return
488 }
489 if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400490 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
491 fmt.Printf("Error: %s\n", err.Error())
492 return
493 }
494 }
495 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
496 fmt.Printf("Error: %s\n", err.Error())
497 }
498 for addr, _ := range s.workers[req.Repository.Name] {
499 go func() {
500 // TODO(gio): make port configurable
501 http.Get(fmt.Sprintf("http://%s/update", addr))
502 }()
gio0eaf2712024-04-14 13:08:46 +0400503 }
504 }()
gio0eaf2712024-04-14 13:08:46 +0400505}
506
gio81246f02024-07-10 12:02:15 +0400507type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400508 Address string `json:"address"`
509}
510
gio8fae3af2024-07-25 13:43:31 +0400511func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400512 vars := mux.Vars(r)
513 appName, ok := vars["app-name"]
514 if !ok || appName == "" {
515 http.Error(w, "missing app-name", http.StatusBadRequest)
516 return
517 }
gio81246f02024-07-10 12:02:15 +0400518 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400519 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
520 http.Error(w, err.Error(), http.StatusInternalServerError)
521 return
522 }
gioa60f0de2024-07-08 10:49:48 +0400523 if _, ok := s.workers[appName]; !ok {
524 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400525 }
gioa60f0de2024-07-08 10:49:48 +0400526 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400527}
528
gio11617ac2024-07-15 16:09:04 +0400529func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
530 u := r.Context().Value(userCtx)
531 if u == nil {
532 http.Error(w, "unauthorized", http.StatusUnauthorized)
533 return
534 }
535 user, ok := u.(string)
536 if !ok {
537 http.Error(w, "could not get user", http.StatusInternalServerError)
538 return
539 }
540 network := r.FormValue("network")
541 if network == "" {
542 http.Error(w, "missing network", http.StatusBadRequest)
543 return
544 }
gio5e49bb62024-07-20 10:43:19 +0400545 subdomain := r.FormValue("subdomain")
546 if subdomain == "" {
547 http.Error(w, "missing subdomain", http.StatusBadRequest)
548 return
549 }
550 appType := r.FormValue("type")
551 if appType == "" {
552 http.Error(w, "missing type", http.StatusBadRequest)
553 return
554 }
gio11617ac2024-07-15 16:09:04 +0400555 g := installer.NewFixedLengthRandomNameGenerator(3)
556 appName, err := g.Generate()
557 if err != nil {
558 http.Error(w, err.Error(), http.StatusInternalServerError)
559 return
560 }
561 if ok, err := s.client.UserExists(user); err != nil {
562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400565 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
566 return
gio11617ac2024-07-15 16:09:04 +0400567 }
giocafd4e62024-07-31 10:53:40 +0400568 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
571 }
572 if err := s.st.CreateApp(appName, user); err != nil {
573 http.Error(w, err.Error(), http.StatusInternalServerError)
574 return
575 }
giod8ab4f52024-07-26 16:58:34 +0400576 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
580 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
581}
582
gio81246f02024-07-10 12:02:15 +0400583type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400584 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400585 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400586 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400587 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400588}
589
gio81246f02024-07-10 12:02:15 +0400590type apiCreateAppResp struct {
591 AppName string `json:"appName"`
592 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400593}
594
gio8fae3af2024-07-25 13:43:31 +0400595func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400596 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400597 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400598 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
599 http.Error(w, err.Error(), http.StatusBadRequest)
600 return
601 }
602 g := installer.NewFixedLengthRandomNameGenerator(3)
603 appName, err := g.Generate()
604 if err != nil {
605 http.Error(w, err.Error(), http.StatusInternalServerError)
606 return
607 }
gio11617ac2024-07-15 16:09:04 +0400608 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400609 if err != nil {
gio33059762024-07-05 13:19:07 +0400610 http.Error(w, err.Error(), http.StatusInternalServerError)
611 return
612 }
gio11617ac2024-07-15 16:09:04 +0400613 if user != "" {
614 http.Error(w, "public key already registered", http.StatusBadRequest)
615 return
616 }
617 user = appName
618 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
619 http.Error(w, err.Error(), http.StatusInternalServerError)
620 return
621 }
622 password := generatePassword()
623 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
624 if err != nil {
625 http.Error(w, err.Error(), http.StatusInternalServerError)
626 return
627 }
giocafd4e62024-07-31 10:53:40 +0400628 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400629 http.Error(w, err.Error(), http.StatusInternalServerError)
630 return
631 }
632 if err := s.st.CreateApp(appName, user); err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
giod8ab4f52024-07-26 16:58:34 +0400636 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400637 http.Error(w, err.Error(), http.StatusInternalServerError)
638 return
639 }
gio81246f02024-07-10 12:02:15 +0400640 resp := apiCreateAppResp{
641 AppName: appName,
642 Password: password,
643 }
gio33059762024-07-05 13:19:07 +0400644 if err := json.NewEncoder(w).Encode(resp); err != nil {
645 http.Error(w, err.Error(), http.StatusInternalServerError)
646 return
647 }
648}
649
giod8ab4f52024-07-26 16:58:34 +0400650func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400651 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400652 return true
653 }
654 for _, cfg := range s.appConfigs {
655 if strings.ToLower(cfg.Network) == network {
656 return false
657 }
658 }
659 return true
660}
661
662func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400663 s.l.Lock()
664 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400665 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400666 network = strings.ToLower(network)
667 if !s.isNetworkUseAllowed(network) {
668 return fmt.Errorf("network already used: %s", network)
669 }
gio33059762024-07-05 13:19:07 +0400670 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400671 return err
gio33059762024-07-05 13:19:07 +0400672 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400673 return nil
gioa60f0de2024-07-08 10:49:48 +0400674 }
gio5e49bb62024-07-20 10:43:19 +0400675 networks, err := s.getNetworks(user)
676 if err != nil {
677 return err
678 }
giod8ab4f52024-07-26 16:58:34 +0400679 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400680 if !ok {
681 return fmt.Errorf("network not found: %s\n", network)
682 }
gio33059762024-07-05 13:19:07 +0400683 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400684 return err
gio33059762024-07-05 13:19:07 +0400685 }
686 appRepo, err := s.client.GetRepo(appName)
687 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400688 return err
gio33059762024-07-05 13:19:07 +0400689 }
gio5e49bb62024-07-20 10:43:19 +0400690 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400691 return err
gio33059762024-07-05 13:19:07 +0400692 }
693 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400694 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
695 if err != nil {
696 return err
697 }
698 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400699 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400700 return err
gio33059762024-07-05 13:19:07 +0400701 }
702 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
703 suffix, err := suffixGen.Generate()
704 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400705 return err
gio33059762024-07-05 13:19:07 +0400706 }
gio94904702024-07-26 16:58:34 +0400707 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400708 s.appConfigs[appName] = appConfig{namespace, network}
gio94904702024-07-26 16:58:34 +0400709 if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400710 return err
gio33059762024-07-05 13:19:07 +0400711 }
giod8ab4f52024-07-26 16:58:34 +0400712 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400713 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400714 return err
gio33059762024-07-05 13:19:07 +0400715 }
716 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400717 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400718 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400719 return err
gio33059762024-07-05 13:19:07 +0400720 }
giod8ab4f52024-07-26 16:58:34 +0400721 if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
722 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400723 if err != nil {
724 return "", err
725 }
726 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400727 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400728 return "", err
729 }
730 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400731 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400732 appName,
733 "/"+appName,
734 namespace,
735 map[string]any{
736 "repoAddr": s.client.GetRepoAddress(appName),
737 "repoHost": strings.Split(s.client.Address(), ":")[0],
738 "gitRepoPublicKey": s.gitRepoPublicKey,
739 },
740 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400741 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400742 installer.WithNoPublish(),
743 installer.WithNoLock(),
744 ); err != nil {
745 return "", err
746 }
747 return fmt.Sprintf("Installed app: %s", appName), nil
748 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400749 return err
gio33059762024-07-05 13:19:07 +0400750 }
751 cfg, err := m.FindInstance(appName)
752 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400753 return err
gio33059762024-07-05 13:19:07 +0400754 }
755 fluxKeys, ok := cfg.Input["fluxKeys"]
756 if !ok {
gio11617ac2024-07-15 16:09:04 +0400757 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400758 }
759 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
760 if !ok {
gio11617ac2024-07-15 16:09:04 +0400761 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400762 }
763 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400764 return err
gio33059762024-07-05 13:19:07 +0400765 } else if ok {
766 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400767 return err
gio33059762024-07-05 13:19:07 +0400768 }
769 } else {
770 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400771 return err
gio33059762024-07-05 13:19:07 +0400772 }
773 }
774 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400775 return err
gio33059762024-07-05 13:19:07 +0400776 }
777 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 +0400778 return err
gio33059762024-07-05 13:19:07 +0400779 }
gio81246f02024-07-10 12:02:15 +0400780 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400781 return err
gio33059762024-07-05 13:19:07 +0400782 }
gio11617ac2024-07-15 16:09:04 +0400783 return nil
gio33059762024-07-05 13:19:07 +0400784}
785
gio81246f02024-07-10 12:02:15 +0400786type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400787 Public string `json:"public"`
788}
789
gio8fae3af2024-07-25 13:43:31 +0400790func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400791 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400792 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
793 http.Error(w, err.Error(), http.StatusBadRequest)
794 return
795 }
796 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
797 http.Error(w, err.Error(), http.StatusInternalServerError)
798 return
799 }
800}
801
gio94904702024-07-26 16:58:34 +0400802type dodoAppRendered struct {
803 App struct {
804 Ingress struct {
805 Network string `json:"network"`
806 Subdomain string `json:"subdomain"`
807 } `json:"ingress"`
808 } `json:"app"`
809 Input struct {
810 AppId string `json:"appId"`
811 } `json:"input"`
812}
813
814func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400815 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400816 if err != nil {
817 return err
818 }
giof8843412024-05-22 16:38:05 +0400819 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400820 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400821 if err != nil {
822 return err
823 }
824 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400825 if err != nil {
826 return err
827 }
828 app, err := installer.NewDodoApp(appCfg)
829 if err != nil {
830 return err
831 }
giof8843412024-05-22 16:38:05 +0400832 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
gio94904702024-07-26 16:58:34 +0400833 return repo.Do(func(r soft.RepoFS) (string, error) {
834 res, err := m.Install(
835 app,
836 "app",
837 "/.dodo/app",
838 namespace,
839 map[string]any{
840 "repoAddr": repo.FullAddress(),
841 "managerAddr": fmt.Sprintf("http://%s", s.self),
842 "appId": name,
843 "sshPrivateKey": s.sshKey,
844 },
845 installer.WithNoPull(),
846 installer.WithNoPublish(),
847 installer.WithConfig(&s.env),
848 installer.WithNetworks(networks),
849 installer.WithLocalChartGenerator(lg),
850 installer.WithNoLock(),
851 )
852 if err != nil {
853 return "", err
854 }
855 var rendered dodoAppRendered
856 if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
857 return "", nil
858 }
859 if _, err := m.Install(
860 appStatus,
861 "status",
862 "/.dodo/status",
863 s.namespace,
864 map[string]any{
865 "appName": rendered.Input.AppId,
866 "network": rendered.App.Ingress.Network,
867 "appSubdomain": rendered.App.Ingress.Subdomain,
868 },
869 installer.WithNoPull(),
870 installer.WithNoPublish(),
871 installer.WithConfig(&s.env),
872 installer.WithNetworks(networks),
873 installer.WithLocalChartGenerator(lg),
874 installer.WithNoLock(),
875 ); err != nil {
876 return "", err
877 }
878 return "install app", nil
879 },
880 soft.WithCommitToBranch("dodo"),
881 soft.WithForce(),
882 )
gio0eaf2712024-04-14 13:08:46 +0400883}
gio33059762024-07-05 13:19:07 +0400884
gio5e49bb62024-07-20 10:43:19 +0400885func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
giob54db242024-07-30 18:49:33 +0400886 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +0400887 appTmpl, err := s.appTmpls.Find(appType)
888 if err != nil {
889 return err
gio33059762024-07-05 13:19:07 +0400890 }
gio33059762024-07-05 13:19:07 +0400891 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400892 if err := appTmpl.Render(network, subdomain, repo); err != nil {
893 return "", err
gio33059762024-07-05 13:19:07 +0400894 }
gio5e49bb62024-07-20 10:43:19 +0400895 return "init", nil
gio33059762024-07-05 13:19:07 +0400896 })
897}
gio81246f02024-07-10 12:02:15 +0400898
899func generatePassword() string {
900 return "foo"
901}
giocb34ad22024-07-11 08:01:13 +0400902
gio11617ac2024-07-15 16:09:04 +0400903func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400904 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400905 resp, err := http.Get(addr)
906 if err != nil {
907 return nil, err
908 }
gio23bdc1b2024-07-11 16:07:47 +0400909 networks := []installer.Network{}
910 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400911 return nil, err
912 }
gio11617ac2024-07-15 16:09:04 +0400913 return s.nf.Filter(user, networks)
914}
915
gio8fae3af2024-07-25 13:43:31 +0400916type publicNetworkData struct {
917 Name string `json:"name"`
918 Domain string `json:"domain"`
919}
920
921type publicData struct {
922 Networks []publicNetworkData `json:"networks"`
923 Types []string `json:"types"`
924}
925
926func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400927 w.Header().Set("Access-Control-Allow-Origin", "*")
928 s.l.Lock()
929 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +0400930 networks, err := s.getNetworks("")
931 if err != nil {
932 http.Error(w, err.Error(), http.StatusInternalServerError)
933 return
934 }
935 var ret publicData
936 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +0400937 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
938 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
939 }
gio8fae3af2024-07-25 13:43:31 +0400940 }
941 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +0400942 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +0400943 }
gio8fae3af2024-07-25 13:43:31 +0400944 if err := json.NewEncoder(w).Encode(ret); err != nil {
945 http.Error(w, err.Error(), http.StatusInternalServerError)
946 return
947 }
948}
949
gio11617ac2024-07-15 16:09:04 +0400950func pickNetwork(networks []installer.Network, network string) []installer.Network {
951 for _, n := range networks {
952 if n.Name == network {
953 return []installer.Network{n}
954 }
955 }
956 return []installer.Network{}
957}
958
959type NetworkFilter interface {
960 Filter(user string, networks []installer.Network) ([]installer.Network, error)
961}
962
963type noNetworkFilter struct{}
964
965func NewNoNetworkFilter() NetworkFilter {
966 return noNetworkFilter{}
967}
968
gio8fae3af2024-07-25 13:43:31 +0400969func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400970 return networks, nil
971}
972
973type filterByOwner struct {
974 st Store
975}
976
977func NewNetworkFilterByOwner(st Store) NetworkFilter {
978 return &filterByOwner{st}
979}
980
981func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +0400982 if user == "" {
983 return networks, nil
984 }
gio11617ac2024-07-15 16:09:04 +0400985 network, err := f.st.GetUserNetwork(user)
986 if err != nil {
987 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400988 }
989 ret := []installer.Network{}
990 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400991 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400992 ret = append(ret, n)
993 }
994 }
giocb34ad22024-07-11 08:01:13 +0400995 return ret, nil
996}
gio11617ac2024-07-15 16:09:04 +0400997
998type allowListFilter struct {
999 allowed []string
1000}
1001
1002func NewAllowListFilter(allowed []string) NetworkFilter {
1003 return &allowListFilter{allowed}
1004}
1005
gio8fae3af2024-07-25 13:43:31 +04001006func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001007 ret := []installer.Network{}
1008 for _, n := range networks {
1009 if slices.Contains(f.allowed, n.Name) {
1010 ret = append(ret, n)
1011 }
1012 }
1013 return ret, nil
1014}
1015
1016type combinedNetworkFilter struct {
1017 filters []NetworkFilter
1018}
1019
1020func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1021 return &combinedNetworkFilter{filters}
1022}
1023
gio8fae3af2024-07-25 13:43:31 +04001024func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001025 ret := networks
1026 var err error
1027 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001028 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001029 if err != nil {
1030 return nil, err
1031 }
1032 }
1033 return ret, nil
1034}
giocafd4e62024-07-31 10:53:40 +04001035
1036type user struct {
1037 Username string `json:"username"`
1038 Email string `json:"email"`
1039 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1040}
1041
1042func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1043 go s.syncUsers()
1044}
1045
1046func (s *DodoAppServer) syncUsers() {
1047 if s.external {
1048 panic("MUST NOT REACH!")
1049 }
1050 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1051 if err != nil {
1052 return
1053 }
1054 users := []user{}
1055 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1056 fmt.Println(err)
1057 return
1058 }
1059 for _, u := range users {
1060 if len(u.SSHPublicKeys) == 0 {
1061 continue
1062 }
1063 if ok, err := s.client.UserExists(u.Username); err != nil {
1064 fmt.Println(err)
1065 return
1066 } else if !ok {
1067 for i, k := range u.SSHPublicKeys {
1068 if i == 0 {
1069 if err := s.client.AddUser(u.Username, k); err != nil {
1070 fmt.Println(err)
1071 return
1072 }
1073 } else {
1074 if err := s.client.AddPublicKey(u.Username, k); err != nil {
1075 fmt.Println(err)
1076 // TODO(dtabidze): If current public key is already registered
1077 // with Git server, this method call will return an error.
1078 // We need to differentiate such errors, and only add key which
1079 // are missing.
1080 continue // return
1081 }
1082 // TODO(dtabidze): Implement RemovePublicKey
1083 }
1084 }
1085 }
1086 }
1087 repos, err := s.client.GetAllRepos()
1088 if err != nil {
1089 return
1090 }
1091 for _, r := range repos {
1092 if r == ConfigRepoName {
1093 continue
1094 }
1095 for _, u := range users {
1096 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1097 fmt.Println(err)
1098 return
1099 }
1100 }
1101 }
1102}