blob: 4307866a43f73b66a2b834d4c33d59fb353621a8 [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"
gio7fbd4ad2024-08-27 10:06:39 +040015 "strconv"
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
Davit Tabidzea5ea5092024-08-01 15:28:09 +040020 "golang.org/x/crypto/bcrypt"
21 "golang.org/x/exp/rand"
22
gio0eaf2712024-04-14 13:08:46 +040023 "github.com/giolekva/pcloud/core/installer"
24 "github.com/giolekva/pcloud/core/installer/soft"
gio43b0f422024-08-21 10:40:13 +040025 "github.com/giolekva/pcloud/core/installer/tasks"
gio33059762024-07-05 13:19:07 +040026
gio7fbd4ad2024-08-27 10:06:39 +040027 "cuelang.org/go/cue"
gio33059762024-07-05 13:19:07 +040028 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040029 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040030)
31
gio23bdc1b2024-07-11 16:07:47 +040032//go:embed dodo-app-tmpl/*
33var dodoAppTmplFS embed.FS
34
gio5e49bb62024-07-20 10:43:19 +040035//go:embed all:app-tmpl
36var appTmplsFS 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"
gio1bf00802024-08-17 12:31:41 +040043 staticPath = "/stat/"
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"
giob4a3a192024-08-19 09:55:47 +040048 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040049)
50
gio23bdc1b2024-07-11 16:07:47 +040051type dodoAppTmplts struct {
giob4a3a192024-08-19 09:55:47 +040052 index *template.Template
53 appStatus *template.Template
54 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040055 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040056}
57
58func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040059 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040060 if err != nil {
61 return dodoAppTmplts{}, err
62 }
gio5e49bb62024-07-20 10:43:19 +040063 parse := func(path string) (*template.Template, error) {
64 if b, err := base.Clone(); err != nil {
65 return nil, err
66 } else {
67 return b.ParseFS(fs, path)
68 }
69 }
70 index, err := parse("dodo-app-tmpl/index.html")
71 if err != nil {
72 return dodoAppTmplts{}, err
73 }
74 appStatus, err := parse("dodo-app-tmpl/app_status.html")
75 if err != nil {
76 return dodoAppTmplts{}, err
77 }
giob4a3a192024-08-19 09:55:47 +040078 commitStatus, err := parse("dodo-app-tmpl/commit_status.html")
79 if err != nil {
80 return dodoAppTmplts{}, err
81 }
gio183e8342024-08-20 06:01:24 +040082 logs, err := parse("dodo-app-tmpl/logs.html")
83 if err != nil {
84 return dodoAppTmplts{}, err
85 }
86 return dodoAppTmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040087}
88
gio0eaf2712024-04-14 13:08:46 +040089type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040090 l sync.Locker
91 st Store
gio11617ac2024-07-15 16:09:04 +040092 nf NetworkFilter
93 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040094 port int
95 apiPort int
96 self string
gio11617ac2024-07-15 16:09:04 +040097 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040098 sshKey string
99 gitRepoPublicKey string
100 client soft.Client
101 namespace string
102 envAppManagerAddr string
103 env installer.EnvConfig
104 nsc installer.NamespaceCreator
105 jc installer.JobCreator
gio864b4332024-09-05 13:56:47 +0400106 vpnKeyGen installer.VPNAPIClient
giof6ad2982024-08-23 17:42:49 +0400107 cnc installer.ClusterNetworkConfigurator
giocb34ad22024-07-11 08:01:13 +0400108 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400109 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +0400110 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +0400111 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400112 external bool
113 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400114 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400115 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400116}
117
118type appConfig struct {
119 Namespace string `json:"namespace"`
120 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400121}
122
gio33059762024-07-05 13:19:07 +0400123// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400124func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400125 st Store,
gio11617ac2024-07-15 16:09:04 +0400126 nf NetworkFilter,
127 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400128 port int,
gioa60f0de2024-07-08 10:49:48 +0400129 apiPort int,
gio33059762024-07-05 13:19:07 +0400130 self string,
gio11617ac2024-07-15 16:09:04 +0400131 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400132 sshKey string,
gio33059762024-07-05 13:19:07 +0400133 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400134 client soft.Client,
135 namespace string,
giocb34ad22024-07-11 08:01:13 +0400136 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400137 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400138 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400139 vpnKeyGen installer.VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +0400140 cnc installer.ClusterNetworkConfigurator,
gio0eaf2712024-04-14 13:08:46 +0400141 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400142 external bool,
143 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400144 reconciler tasks.Reconciler,
gio9d66f322024-07-06 13:45:10 +0400145) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400146 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
147 if err != nil {
148 return nil, err
149 }
gio5e49bb62024-07-20 10:43:19 +0400150 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
151 if err != nil {
152 return nil, err
153 }
154 appTmpls, err := NewAppTmplStoreFS(apps)
155 if err != nil {
156 return nil, err
157 }
gio9d66f322024-07-06 13:45:10 +0400158 s := &DodoAppServer{
159 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400160 st,
gio11617ac2024-07-15 16:09:04 +0400161 nf,
162 ug,
gio0eaf2712024-04-14 13:08:46 +0400163 port,
gioa60f0de2024-07-08 10:49:48 +0400164 apiPort,
gio33059762024-07-05 13:19:07 +0400165 self,
gio11617ac2024-07-15 16:09:04 +0400166 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400167 sshKey,
gio33059762024-07-05 13:19:07 +0400168 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400169 client,
170 namespace,
giocb34ad22024-07-11 08:01:13 +0400171 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400172 env,
gio33059762024-07-05 13:19:07 +0400173 nsc,
giof8843412024-05-22 16:38:05 +0400174 jc,
gio36b23b32024-08-25 12:20:54 +0400175 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +0400176 cnc,
gio266c04f2024-07-03 14:18:45 +0400177 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400178 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400179 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400180 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400181 external,
182 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400183 reconciler,
gio183e8342024-08-20 06:01:24 +0400184 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400185 }
gioa60f0de2024-07-08 10:49:48 +0400186 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400187 if err != nil {
188 return nil, err
189 }
giod8ab4f52024-07-26 16:58:34 +0400190 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400191 if err == nil {
192 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400193 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400194 return nil, err
195 }
196 } else if !errors.Is(err, fs.ErrNotExist) {
197 return nil, err
198 }
199 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400200}
201
gio7fbd4ad2024-08-27 10:06:39 +0400202func (s *DodoAppServer) getAppConfig(app, branch string) appConfig {
203 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
204}
205
206func (s *DodoAppServer) setAppConfig(app, branch string, cfg appConfig) {
207 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
208}
209
gio0eaf2712024-04-14 13:08:46 +0400210func (s *DodoAppServer) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400211 // if err := s.client.DisableKeyless(); err != nil {
212 // return err
213 // }
214 // if err := s.client.DisableAnonAccess(); err != nil {
215 // return err
216 // }
gioa60f0de2024-07-08 10:49:48 +0400217 e := make(chan error)
218 go func() {
219 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400220 r.Use(s.mwAuth)
gio1bf00802024-08-17 12:31:41 +0400221 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400222 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400223 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
224 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400225 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
226 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400227 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400228 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400229 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
230 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio81246f02024-07-10 12:02:15 +0400231 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
232 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400233 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400234 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
235 }()
236 go func() {
237 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400238 r.HandleFunc("/update", s.handleAPIUpdate)
239 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400240 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400241 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400242 if !s.external {
243 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
244 }
gioa60f0de2024-07-08 10:49:48 +0400245 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
246 }()
giocafd4e62024-07-31 10:53:40 +0400247 if !s.external {
248 go func() {
249 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400250 for {
251 delay := time.Duration(rand.Intn(60)+60) * time.Second
252 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400253 s.syncUsers()
254 }
255 }()
256 }
gioa60f0de2024-07-08 10:49:48 +0400257 return <-e
258}
259
gio11617ac2024-07-15 16:09:04 +0400260type UserGetter interface {
261 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400262 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400263}
264
265type externalUserGetter struct {
266 sc *securecookie.SecureCookie
267}
268
269func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400270 return &externalUserGetter{securecookie.New(
271 securecookie.GenerateRandomKey(64),
272 securecookie.GenerateRandomKey(32),
273 )}
gio11617ac2024-07-15 16:09:04 +0400274}
275
276func (ug *externalUserGetter) Get(r *http.Request) string {
277 cookie, err := r.Cookie(sessionCookie)
278 if err != nil {
279 return ""
280 }
281 var user string
282 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
283 return ""
284 }
285 return user
286}
287
gio8fae3af2024-07-25 13:43:31 +0400288func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
289 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
290 cookie := &http.Cookie{
291 Name: sessionCookie,
292 Value: encoded,
293 Path: "/",
294 Secure: true,
295 HttpOnly: true,
296 }
297 http.SetCookie(w, cookie)
298 return nil
299 } else {
300 return err
301 }
302}
303
gio11617ac2024-07-15 16:09:04 +0400304type internalUserGetter struct{}
305
306func NewInternalUserGetter() UserGetter {
307 return internalUserGetter{}
308}
309
310func (ug internalUserGetter) Get(r *http.Request) string {
311 return r.Header.Get("X-User")
312}
313
gio8fae3af2024-07-25 13:43:31 +0400314func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
315 return nil
316}
317
gio81246f02024-07-10 12:02:15 +0400318func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
319 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400320 if strings.HasSuffix(r.URL.Path, loginPath) ||
321 strings.HasPrefix(r.URL.Path, logoutPath) ||
322 strings.HasPrefix(r.URL.Path, staticPath) ||
323 strings.HasPrefix(r.URL.Path, apiPublicData) ||
324 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400325 next.ServeHTTP(w, r)
326 return
327 }
gio11617ac2024-07-15 16:09:04 +0400328 user := s.ug.Get(r)
329 if user == "" {
gio81246f02024-07-10 12:02:15 +0400330 vars := mux.Vars(r)
331 appName, ok := vars["app-name"]
332 if !ok || appName == "" {
333 http.Error(w, "missing app-name", http.StatusBadRequest)
334 return
335 }
336 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
337 return
338 }
gio81246f02024-07-10 12:02:15 +0400339 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
340 })
341}
342
343func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400344 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400345 http.SetCookie(w, &http.Cookie{
346 Name: sessionCookie,
347 Value: "",
348 Path: "/",
349 HttpOnly: true,
350 Secure: true,
351 })
352 http.Redirect(w, r, "/", http.StatusSeeOther)
353}
354
355func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
356 vars := mux.Vars(r)
357 appName, ok := vars["app-name"]
358 if !ok || appName == "" {
359 http.Error(w, "missing app-name", http.StatusBadRequest)
360 return
361 }
362 fmt.Fprint(w, `
363<!DOCTYPE html>
364<html lang='en'>
365 <head>
366 <title>dodo: app - login</title>
367 <meta charset='utf-8'>
368 </head>
369 <body>
370 <form action="" method="POST">
371 <input type="password" placeholder="Password" name="password" required />
372 <button type="submit">Login</button>
373 </form>
374 </body>
375</html>
376`)
377}
378
379func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
380 vars := mux.Vars(r)
381 appName, ok := vars["app-name"]
382 if !ok || appName == "" {
383 http.Error(w, "missing app-name", http.StatusBadRequest)
384 return
385 }
386 password := r.FormValue("password")
387 if password == "" {
388 http.Error(w, "missing password", http.StatusBadRequest)
389 return
390 }
391 user, err := s.st.GetAppOwner(appName)
392 if err != nil {
393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
395 }
396 hashed, err := s.st.GetUserPassword(user)
397 if err != nil {
398 http.Error(w, err.Error(), http.StatusInternalServerError)
399 return
400 }
401 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
402 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
403 return
404 }
gio8fae3af2024-07-25 13:43:31 +0400405 if err := s.ug.Encode(w, user); err != nil {
406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 return
gio81246f02024-07-10 12:02:15 +0400408 }
409 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
410}
411
giob4a3a192024-08-19 09:55:47 +0400412type navItem struct {
413 Name string
414 Address string
415}
416
gio23bdc1b2024-07-11 16:07:47 +0400417type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400418 Navigation []navItem
419 Apps []string
420 Networks []installer.Network
421 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400422}
423
gioa60f0de2024-07-08 10:49:48 +0400424func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400425 user := r.Context().Value(userCtx)
426 if user == nil {
427 http.Error(w, "unauthorized", http.StatusUnauthorized)
428 return
429 }
430 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400431 if err != nil {
432 http.Error(w, err.Error(), http.StatusInternalServerError)
433 return
434 }
gio11617ac2024-07-15 16:09:04 +0400435 networks, err := s.getNetworks(user.(string))
436 if err != nil {
437 http.Error(w, err.Error(), http.StatusInternalServerError)
438 return
439 }
giob54db242024-07-30 18:49:33 +0400440 var types []string
441 for _, t := range s.appTmpls.Types() {
442 types = append(types, strings.Replace(t, "-", ":", 1))
443 }
giob4a3a192024-08-19 09:55:47 +0400444 n := []navItem{navItem{"Home", "/"}}
445 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400446 if err := s.tmplts.index.Execute(w, data); err != nil {
447 http.Error(w, err.Error(), http.StatusInternalServerError)
448 return
gioa60f0de2024-07-08 10:49:48 +0400449 }
450}
451
gio5e49bb62024-07-20 10:43:19 +0400452type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400453 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400454 Name string
455 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400456 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400457 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400458 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400459}
460
gioa60f0de2024-07-08 10:49:48 +0400461func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
462 vars := mux.Vars(r)
463 appName, ok := vars["app-name"]
464 if !ok || appName == "" {
465 http.Error(w, "missing app-name", http.StatusBadRequest)
466 return
467 }
gio7fbd4ad2024-08-27 10:06:39 +0400468 branch, ok := vars["branch"]
469 if !ok || branch == "" {
470 branch = "master"
471 }
gio94904702024-07-26 16:58:34 +0400472 u := r.Context().Value(userCtx)
473 if u == nil {
474 http.Error(w, "unauthorized", http.StatusUnauthorized)
475 return
476 }
477 user, ok := u.(string)
478 if !ok {
479 http.Error(w, "could not get user", http.StatusInternalServerError)
480 return
481 }
482 owner, err := s.st.GetAppOwner(appName)
483 if err != nil {
484 http.Error(w, err.Error(), http.StatusInternalServerError)
485 return
486 }
487 if owner != user {
488 http.Error(w, "unauthorized", http.StatusUnauthorized)
489 return
490 }
gio7fbd4ad2024-08-27 10:06:39 +0400491 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400492 if err != nil {
493 http.Error(w, err.Error(), http.StatusInternalServerError)
494 return
495 }
gio183e8342024-08-20 06:01:24 +0400496 var lastCommitResources resourceData
497 if len(commits) > 0 {
498 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
499 if err != nil {
500 http.Error(w, err.Error(), http.StatusInternalServerError)
501 return
502 }
503 r, err := extractResourceData(lastCommit.Resources.Helm)
504 if err != nil {
505 http.Error(w, err.Error(), http.StatusInternalServerError)
506 return
507 }
508 lastCommitResources = r
509 }
gio7fbd4ad2024-08-27 10:06:39 +0400510 branches, err := s.st.GetBranches(appName)
511 if err != nil {
512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
514 }
gio5e49bb62024-07-20 10:43:19 +0400515 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400516 Navigation: []navItem{
517 navItem{"Home", "/"},
518 navItem{appName, "/" + appName},
519 },
gio5e49bb62024-07-20 10:43:19 +0400520 Name: appName,
521 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
522 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400523 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400524 Branches: branches,
525 }
526 if branch != "master" {
527 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400528 }
529 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
530 http.Error(w, err.Error(), http.StatusInternalServerError)
531 return
gioa60f0de2024-07-08 10:49:48 +0400532 }
gio0eaf2712024-04-14 13:08:46 +0400533}
534
giocfb228c2024-09-06 15:44:31 +0400535type appEnv struct {
536 Profile string `json:"envProfile"`
537}
538
539func (s *DodoAppServer) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
540 vars := mux.Vars(r)
541 appName, ok := vars["app-name"]
542 if !ok || appName == "" {
543 http.Error(w, "missing app-name", http.StatusBadRequest)
544 return
545 }
546 branch, ok := vars["branch"]
547 if !ok || branch == "" {
548 branch = "master"
549 }
550 info, err := s.st.GetLastCommitInfo(appName, branch)
551 if err != nil {
552 http.Error(w, err.Error(), http.StatusInternalServerError)
553 return
554 }
555 var e appEnv
556 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
560 fmt.Fprintln(w, e.Profile)
561}
562
giob4a3a192024-08-19 09:55:47 +0400563type volume struct {
564 Name string
565 Size string
566}
567
568type postgresql struct {
569 Name string
570 Version string
571 Volume string
572}
573
574type ingress struct {
575 Host string
576}
577
gio7fbd4ad2024-08-27 10:06:39 +0400578type vm struct {
579 Name string
580 User string
581 CPUCores int
582 Memory string
583}
584
giob4a3a192024-08-19 09:55:47 +0400585type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400586 Volume []volume
587 PostgreSQL []postgresql
588 Ingress []ingress
589 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400590}
591
592type commitStatusData struct {
593 Navigation []navItem
594 AppName string
595 Commit Commit
596 Resources resourceData
597}
598
599func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
600 vars := mux.Vars(r)
601 appName, ok := vars["app-name"]
602 if !ok || appName == "" {
603 http.Error(w, "missing app-name", http.StatusBadRequest)
604 return
605 }
606 hash, ok := vars["hash"]
607 if !ok || appName == "" {
608 http.Error(w, "missing app-name", http.StatusBadRequest)
609 return
610 }
611 u := r.Context().Value(userCtx)
612 if u == nil {
613 http.Error(w, "unauthorized", http.StatusUnauthorized)
614 return
615 }
616 user, ok := u.(string)
617 if !ok {
618 http.Error(w, "could not get user", http.StatusInternalServerError)
619 return
620 }
621 owner, err := s.st.GetAppOwner(appName)
622 if err != nil {
623 http.Error(w, err.Error(), http.StatusInternalServerError)
624 return
625 }
626 if owner != user {
627 http.Error(w, "unauthorized", http.StatusUnauthorized)
628 return
629 }
630 commit, err := s.st.GetCommit(hash)
631 if err != nil {
632 // TODO(gio): not-found ?
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
636 var res strings.Builder
637 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
638 http.Error(w, err.Error(), http.StatusInternalServerError)
639 return
640 }
641 resData, err := extractResourceData(commit.Resources.Helm)
642 if err != nil {
643 http.Error(w, err.Error(), http.StatusInternalServerError)
644 return
645 }
646 data := commitStatusData{
647 Navigation: []navItem{
648 navItem{"Home", "/"},
649 navItem{appName, "/" + appName},
650 navItem{hash, "/" + appName + "/" + hash},
651 },
652 AppName: appName,
653 Commit: commit,
654 Resources: resData,
655 }
656 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
657 http.Error(w, err.Error(), http.StatusInternalServerError)
658 return
659 }
660}
661
gio183e8342024-08-20 06:01:24 +0400662type logData struct {
663 Navigation []navItem
664 AppName string
665 Logs template.HTML
666}
667
668func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
669 vars := mux.Vars(r)
670 appName, ok := vars["app-name"]
671 if !ok || appName == "" {
672 http.Error(w, "missing app-name", http.StatusBadRequest)
673 return
674 }
675 u := r.Context().Value(userCtx)
676 if u == nil {
677 http.Error(w, "unauthorized", http.StatusUnauthorized)
678 return
679 }
680 user, ok := u.(string)
681 if !ok {
682 http.Error(w, "could not get user", http.StatusInternalServerError)
683 return
684 }
685 owner, err := s.st.GetAppOwner(appName)
686 if err != nil {
687 http.Error(w, err.Error(), http.StatusInternalServerError)
688 return
689 }
690 if owner != user {
691 http.Error(w, "unauthorized", http.StatusUnauthorized)
692 return
693 }
694 data := logData{
695 Navigation: []navItem{
696 navItem{"Home", "/"},
697 navItem{appName, "/" + appName},
698 navItem{"Logs", "/" + appName + "/logs"},
699 },
700 AppName: appName,
701 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
702 }
703 if err := s.tmplts.logs.Execute(w, data); err != nil {
704 fmt.Println(err)
705 http.Error(w, err.Error(), http.StatusInternalServerError)
706 return
707 }
708}
709
gio81246f02024-07-10 12:02:15 +0400710type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400711 Ref string `json:"ref"`
712 Repository struct {
713 Name string `json:"name"`
714 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400715 After string `json:"after"`
716 Commits []struct {
717 Id string `json:"id"`
718 Message string `json:"message"`
719 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400720}
721
gio8fae3af2024-07-25 13:43:31 +0400722func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400723 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400724 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400725 var contents strings.Builder
726 io.Copy(&contents, r.Body)
727 c := contents.String()
728 fmt.Println(c)
729 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400730 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400731 return
732 }
gio7fbd4ad2024-08-27 10:06:39 +0400733 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
734 return
735 }
736 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
737 if !ok {
738 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400739 return
740 }
gioa60f0de2024-07-08 10:49:48 +0400741 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400742 go func() {
gio11617ac2024-07-15 16:09:04 +0400743 owner, err := s.st.GetAppOwner(req.Repository.Name)
744 if err != nil {
745 return
746 }
747 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400748 if err != nil {
749 return
750 }
giof15b9da2024-09-19 06:59:16 +0400751 // TODO(gio): get only available ones by owner
752 clusters, err := s.getClusters()
753 if err != nil {
754 return
755 }
gio94904702024-07-26 16:58:34 +0400756 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
757 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
758 if err != nil {
759 return
760 }
gioe2e31e12024-08-18 08:20:56 +0400761 found := false
762 commitMsg := ""
763 for _, c := range req.Commits {
764 if c.Id == req.After {
765 found = true
766 commitMsg = c.Message
767 break
gioa60f0de2024-07-08 10:49:48 +0400768 }
769 }
gioe2e31e12024-08-18 08:20:56 +0400770 if !found {
771 fmt.Printf("Error: could not find commit message")
772 return
773 }
gio7fbd4ad2024-08-27 10:06:39 +0400774 s.l.Lock()
775 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400776 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, clusters, owner)
gio7fbd4ad2024-08-27 10:06:39 +0400777 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400778 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400779 return
780 }
gioa60f0de2024-07-08 10:49:48 +0400781 for addr, _ := range s.workers[req.Repository.Name] {
782 go func() {
783 // TODO(gio): make port configurable
784 http.Get(fmt.Sprintf("http://%s/update", addr))
785 }()
gio0eaf2712024-04-14 13:08:46 +0400786 }
787 }()
gio0eaf2712024-04-14 13:08:46 +0400788}
789
gio81246f02024-07-10 12:02:15 +0400790type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400791 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400792 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400793}
794
gio8fae3af2024-07-25 13:43:31 +0400795func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400796 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400797 vars := mux.Vars(r)
798 appName, ok := vars["app-name"]
799 if !ok || appName == "" {
800 http.Error(w, "missing app-name", http.StatusBadRequest)
801 return
802 }
gio81246f02024-07-10 12:02:15 +0400803 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400804 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
805 http.Error(w, err.Error(), http.StatusInternalServerError)
806 return
807 }
gioa60f0de2024-07-08 10:49:48 +0400808 if _, ok := s.workers[appName]; !ok {
809 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400810 }
gioa60f0de2024-07-08 10:49:48 +0400811 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400812 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400813}
814
gio11617ac2024-07-15 16:09:04 +0400815func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
816 u := r.Context().Value(userCtx)
817 if u == nil {
818 http.Error(w, "unauthorized", http.StatusUnauthorized)
819 return
820 }
821 user, ok := u.(string)
822 if !ok {
823 http.Error(w, "could not get user", http.StatusInternalServerError)
824 return
825 }
826 network := r.FormValue("network")
827 if network == "" {
828 http.Error(w, "missing network", http.StatusBadRequest)
829 return
830 }
gio5e49bb62024-07-20 10:43:19 +0400831 subdomain := r.FormValue("subdomain")
832 if subdomain == "" {
833 http.Error(w, "missing subdomain", http.StatusBadRequest)
834 return
835 }
836 appType := r.FormValue("type")
837 if appType == "" {
838 http.Error(w, "missing type", http.StatusBadRequest)
839 return
840 }
gio11617ac2024-07-15 16:09:04 +0400841 g := installer.NewFixedLengthRandomNameGenerator(3)
842 appName, err := g.Generate()
843 if err != nil {
844 http.Error(w, err.Error(), http.StatusInternalServerError)
845 return
846 }
847 if ok, err := s.client.UserExists(user); err != nil {
848 http.Error(w, err.Error(), http.StatusInternalServerError)
849 return
850 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400851 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
852 return
gio11617ac2024-07-15 16:09:04 +0400853 }
giocafd4e62024-07-31 10:53:40 +0400854 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400855 http.Error(w, err.Error(), http.StatusInternalServerError)
856 return
857 }
858 if err := s.st.CreateApp(appName, user); err != nil {
859 http.Error(w, err.Error(), http.StatusInternalServerError)
860 return
861 }
giod8ab4f52024-07-26 16:58:34 +0400862 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400863 http.Error(w, err.Error(), http.StatusInternalServerError)
864 return
865 }
866 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
867}
868
gio7fbd4ad2024-08-27 10:06:39 +0400869func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
870 u := r.Context().Value(userCtx)
871 if u == nil {
872 http.Error(w, "unauthorized", http.StatusUnauthorized)
873 return
874 }
875 user, ok := u.(string)
876 if !ok {
877 http.Error(w, "could not get user", http.StatusInternalServerError)
878 return
879 }
880 vars := mux.Vars(r)
881 appName, ok := vars["app-name"]
882 if !ok || appName == "" {
883 http.Error(w, "missing app-name", http.StatusBadRequest)
884 return
885 }
886 branch := r.FormValue("branch")
887 if branch == "" {
888 http.Error(w, "missing network", http.StatusBadRequest)
889 return
890 }
891 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
892 http.Error(w, err.Error(), http.StatusInternalServerError)
893 return
894 }
895 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
896}
897
gio81246f02024-07-10 12:02:15 +0400898type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400899 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400900 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400901 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400902 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400903}
904
gio81246f02024-07-10 12:02:15 +0400905type apiCreateAppResp struct {
906 AppName string `json:"appName"`
907 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400908}
909
gio8fae3af2024-07-25 13:43:31 +0400910func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400911 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400912 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400913 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
914 http.Error(w, err.Error(), http.StatusBadRequest)
915 return
916 }
917 g := installer.NewFixedLengthRandomNameGenerator(3)
918 appName, err := g.Generate()
919 if err != nil {
920 http.Error(w, err.Error(), http.StatusInternalServerError)
921 return
922 }
gio11617ac2024-07-15 16:09:04 +0400923 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400924 if err != nil {
gio33059762024-07-05 13:19:07 +0400925 http.Error(w, err.Error(), http.StatusInternalServerError)
926 return
927 }
gio11617ac2024-07-15 16:09:04 +0400928 if user != "" {
929 http.Error(w, "public key already registered", http.StatusBadRequest)
930 return
931 }
932 user = appName
933 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
934 http.Error(w, err.Error(), http.StatusInternalServerError)
935 return
936 }
937 password := generatePassword()
938 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
939 if err != nil {
940 http.Error(w, err.Error(), http.StatusInternalServerError)
941 return
942 }
giocafd4e62024-07-31 10:53:40 +0400943 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400944 http.Error(w, err.Error(), http.StatusInternalServerError)
945 return
946 }
947 if err := s.st.CreateApp(appName, user); err != nil {
948 http.Error(w, err.Error(), http.StatusInternalServerError)
949 return
950 }
giod8ab4f52024-07-26 16:58:34 +0400951 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400952 http.Error(w, err.Error(), http.StatusInternalServerError)
953 return
954 }
gio81246f02024-07-10 12:02:15 +0400955 resp := apiCreateAppResp{
956 AppName: appName,
957 Password: password,
958 }
gio33059762024-07-05 13:19:07 +0400959 if err := json.NewEncoder(w).Encode(resp); err != nil {
960 http.Error(w, err.Error(), http.StatusInternalServerError)
961 return
962 }
963}
964
giod8ab4f52024-07-26 16:58:34 +0400965func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400966 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400967 return true
968 }
969 for _, cfg := range s.appConfigs {
970 if strings.ToLower(cfg.Network) == network {
971 return false
972 }
973 }
974 return true
975}
976
977func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400978 s.l.Lock()
979 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400980 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400981 network = strings.ToLower(network)
982 if !s.isNetworkUseAllowed(network) {
983 return fmt.Errorf("network already used: %s", network)
984 }
gio33059762024-07-05 13:19:07 +0400985 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400986 return err
gio33059762024-07-05 13:19:07 +0400987 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400988 return nil
gioa60f0de2024-07-08 10:49:48 +0400989 }
gio5e49bb62024-07-20 10:43:19 +0400990 networks, err := s.getNetworks(user)
991 if err != nil {
992 return err
993 }
giod8ab4f52024-07-26 16:58:34 +0400994 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400995 if !ok {
996 return fmt.Errorf("network not found: %s\n", network)
997 }
gio33059762024-07-05 13:19:07 +0400998 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400999 return err
gio33059762024-07-05 13:19:07 +04001000 }
1001 appRepo, err := s.client.GetRepo(appName)
1002 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001003 return err
gio33059762024-07-05 13:19:07 +04001004 }
gio7fbd4ad2024-08-27 10:06:39 +04001005 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
1006 if err != nil {
1007 return err
1008 }
1009 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1010}
1011
1012func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
1013 s.l.Lock()
1014 defer s.l.Unlock()
1015 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1016 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1017 if err != nil {
1018 return err
1019 }
1020 appCfg, err := soft.ReadFile(appRepo, "app.cue")
1021 if err != nil {
1022 return err
1023 }
1024 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1025 if err != nil {
1026 return err
1027 }
1028 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
1029}
1030
1031func (s *DodoAppServer) createAppForBranch(
1032 repo soft.RepoIO,
1033 appName string,
1034 branch string,
1035 user string,
1036 network string,
1037 files map[string][]byte,
1038) error {
1039 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1040 for path, contents := range files {
1041 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1042 return "", err
1043 }
1044 }
1045 return "init", nil
1046 }, soft.WithCommitToBranch(branch))
1047 if err != nil {
1048 return err
1049 }
1050 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001051 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001052 return err
gio33059762024-07-05 13:19:07 +04001053 }
giof15b9da2024-09-19 06:59:16 +04001054 // TODO(gio): get only available ones by owner
1055 clusters, err := s.getClusters()
1056 if err != nil {
1057 return err
1058 }
gio33059762024-07-05 13:19:07 +04001059 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001060 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1061 if err != nil {
1062 return err
1063 }
1064 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001065 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001066 return err
gio33059762024-07-05 13:19:07 +04001067 }
1068 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1069 suffix, err := suffixGen.Generate()
1070 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001071 return err
gio33059762024-07-05 13:19:07 +04001072 }
gio94904702024-07-26 16:58:34 +04001073 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001074 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001075 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001076 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001077 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001078 return err
1079 }
gio7fbd4ad2024-08-27 10:06:39 +04001080 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001081 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001082 return err
gio33059762024-07-05 13:19:07 +04001083 }
giod8ab4f52024-07-26 16:58:34 +04001084 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001085 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001086 return err
gio33059762024-07-05 13:19:07 +04001087 }
1088 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001089 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001090 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001091 return err
gio33059762024-07-05 13:19:07 +04001092 }
gio7fbd4ad2024-08-27 10:06:39 +04001093 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001094 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001095 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001096 if err != nil {
1097 return "", err
1098 }
1099 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001100 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001101 return "", err
1102 }
1103 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001104 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001105 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001106 appPath,
gio9d66f322024-07-06 13:45:10 +04001107 namespace,
1108 map[string]any{
1109 "repoAddr": s.client.GetRepoAddress(appName),
1110 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001111 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001112 "gitRepoPublicKey": s.gitRepoPublicKey,
1113 },
1114 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001115 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001116 installer.WithNoPublish(),
1117 installer.WithNoLock(),
1118 ); err != nil {
1119 return "", err
1120 }
1121 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001122 })
1123 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001124 return err
gio33059762024-07-05 13:19:07 +04001125 }
gio7fbd4ad2024-08-27 10:06:39 +04001126 return s.initAppACLs(m, appPath, appName, branch, user)
1127}
1128
1129func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
1130 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001131 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001132 return err
gio33059762024-07-05 13:19:07 +04001133 }
1134 fluxKeys, ok := cfg.Input["fluxKeys"]
1135 if !ok {
gio11617ac2024-07-15 16:09:04 +04001136 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001137 }
1138 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1139 if !ok {
gio11617ac2024-07-15 16:09:04 +04001140 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001141 }
1142 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001143 return err
gio33059762024-07-05 13:19:07 +04001144 } else if ok {
1145 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001146 return err
gio33059762024-07-05 13:19:07 +04001147 }
1148 } else {
1149 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001150 return err
gio33059762024-07-05 13:19:07 +04001151 }
1152 }
gio7fbd4ad2024-08-27 10:06:39 +04001153 if branch != "master" {
1154 return nil
1155 }
gio33059762024-07-05 13:19:07 +04001156 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001157 return err
gio33059762024-07-05 13:19:07 +04001158 }
gio7fbd4ad2024-08-27 10:06:39 +04001159 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001160 return err
gio33059762024-07-05 13:19:07 +04001161 }
gio7fbd4ad2024-08-27 10:06:39 +04001162 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 +04001163 return err
gio33059762024-07-05 13:19:07 +04001164 }
gio2ccb6e32024-08-15 12:01:33 +04001165 if !s.external {
1166 go func() {
1167 users, err := s.client.GetAllUsers()
1168 if err != nil {
1169 fmt.Println(err)
1170 return
1171 }
1172 for _, user := range users {
1173 // TODO(gio): fluxcd should have only read access
1174 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1175 fmt.Println(err)
1176 }
1177 }
1178 }()
1179 }
gio43b0f422024-08-21 10:40:13 +04001180 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1181 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001182 return nil
gio33059762024-07-05 13:19:07 +04001183}
1184
gio81246f02024-07-10 12:02:15 +04001185type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001186 User string `json:"user"`
1187 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001188}
1189
gio7fbd4ad2024-08-27 10:06:39 +04001190func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001191 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001192 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1193 http.Error(w, err.Error(), http.StatusBadRequest)
1194 return
1195 }
gio7fbd4ad2024-08-27 10:06:39 +04001196 if req.User == "" {
1197 http.Error(w, "invalid user", http.StatusBadRequest)
1198 return
1199 }
1200 if req.PublicKey == "" {
1201 http.Error(w, "invalid public key", http.StatusBadRequest)
1202 return
1203 }
1204 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001205 http.Error(w, err.Error(), http.StatusInternalServerError)
1206 return
1207 }
1208}
1209
gio94904702024-07-26 16:58:34 +04001210type dodoAppRendered struct {
1211 App struct {
1212 Ingress struct {
1213 Network string `json:"network"`
1214 Subdomain string `json:"subdomain"`
1215 } `json:"ingress"`
1216 } `json:"app"`
1217 Input struct {
1218 AppId string `json:"appId"`
1219 } `json:"input"`
1220}
1221
gio7fbd4ad2024-08-27 10:06:39 +04001222// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio43b0f422024-08-21 10:40:13 +04001223func (s *DodoAppServer) updateDodoApp(
1224 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001225 name string,
1226 branch string,
1227 namespace string,
gio43b0f422024-08-21 10:40:13 +04001228 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001229 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001230 owner string,
gio43b0f422024-08-21 10:40:13 +04001231) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001232 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001233 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001234 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001235 }
giof8843412024-05-22 16:38:05 +04001236 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001237 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001238 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001239 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001240 }
1241 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +04001242 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001243 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001244 }
1245 app, err := installer.NewDodoApp(appCfg)
1246 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001247 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001248 }
giof8843412024-05-22 16:38:05 +04001249 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001250 var ret installer.ReleaseResources
1251 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1252 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001253 app,
1254 "app",
1255 "/.dodo/app",
1256 namespace,
1257 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001258 "repoAddr": repo.FullAddress(),
1259 "repoPublicAddr": s.repoPublicAddr,
1260 "managerAddr": fmt.Sprintf("http://%s", s.self),
1261 "appId": name,
1262 "branch": branch,
1263 "sshPrivateKey": s.sshKey,
1264 "username": owner,
gio94904702024-07-26 16:58:34 +04001265 },
1266 installer.WithNoPull(),
1267 installer.WithNoPublish(),
1268 installer.WithConfig(&s.env),
1269 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001270 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001271 installer.WithLocalChartGenerator(lg),
1272 installer.WithNoLock(),
1273 )
1274 if err != nil {
1275 return "", err
1276 }
1277 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001278 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001279 return "", nil
1280 }
1281 if _, err := m.Install(
1282 appStatus,
1283 "status",
1284 "/.dodo/status",
1285 s.namespace,
1286 map[string]any{
1287 "appName": rendered.Input.AppId,
1288 "network": rendered.App.Ingress.Network,
1289 "appSubdomain": rendered.App.Ingress.Subdomain,
1290 },
1291 installer.WithNoPull(),
1292 installer.WithNoPublish(),
1293 installer.WithConfig(&s.env),
1294 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001295 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001296 installer.WithLocalChartGenerator(lg),
1297 installer.WithNoLock(),
1298 ); err != nil {
1299 return "", err
1300 }
1301 return "install app", nil
1302 },
gio7fbd4ad2024-08-27 10:06:39 +04001303 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001304 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001305 ); err != nil {
1306 return installer.ReleaseResources{}, err
1307 }
gio43b0f422024-08-21 10:40:13 +04001308 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1309 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001310 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001311}
gio33059762024-07-05 13:19:07 +04001312
gio7fbd4ad2024-08-27 10:06:39 +04001313func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001314 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001315 appTmpl, err := s.appTmpls.Find(appType)
1316 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001317 return nil, err
gio33059762024-07-05 13:19:07 +04001318 }
gio7fbd4ad2024-08-27 10:06:39 +04001319 return appTmpl.Render(network, subdomain)
gio33059762024-07-05 13:19:07 +04001320}
gio81246f02024-07-10 12:02:15 +04001321
1322func generatePassword() string {
1323 return "foo"
1324}
giocb34ad22024-07-11 08:01:13 +04001325
gio11617ac2024-07-15 16:09:04 +04001326func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001327 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001328 resp, err := http.Get(addr)
1329 if err != nil {
1330 return nil, err
1331 }
gio23bdc1b2024-07-11 16:07:47 +04001332 networks := []installer.Network{}
1333 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001334 return nil, err
1335 }
gio11617ac2024-07-15 16:09:04 +04001336 return s.nf.Filter(user, networks)
1337}
1338
giof15b9da2024-09-19 06:59:16 +04001339func (s *DodoAppServer) getClusters() ([]installer.Cluster, error) {
1340 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1341 resp, err := http.Get(addr)
1342 if err != nil {
1343 return nil, err
1344 }
1345 clusters := []installer.Cluster{}
1346 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1347 return nil, err
1348 }
1349 fmt.Printf("CLUSTERS %+v\n", clusters)
1350 return clusters, nil
1351}
1352
gio8fae3af2024-07-25 13:43:31 +04001353type publicNetworkData struct {
1354 Name string `json:"name"`
1355 Domain string `json:"domain"`
1356}
1357
1358type publicData struct {
1359 Networks []publicNetworkData `json:"networks"`
1360 Types []string `json:"types"`
1361}
1362
1363func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001364 w.Header().Set("Access-Control-Allow-Origin", "*")
1365 s.l.Lock()
1366 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001367 networks, err := s.getNetworks("")
1368 if err != nil {
1369 http.Error(w, err.Error(), http.StatusInternalServerError)
1370 return
1371 }
1372 var ret publicData
1373 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001374 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1375 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1376 }
gio8fae3af2024-07-25 13:43:31 +04001377 }
1378 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001379 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001380 }
gio8fae3af2024-07-25 13:43:31 +04001381 if err := json.NewEncoder(w).Encode(ret); err != nil {
1382 http.Error(w, err.Error(), http.StatusInternalServerError)
1383 return
1384 }
1385}
1386
gio7fbd4ad2024-08-27 10:06:39 +04001387func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001388 if err != nil {
1389 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001390 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001391 fmt.Printf("Error: %s\n", err.Error())
1392 return err
1393 }
1394 return err
1395 }
1396 var resB bytes.Buffer
1397 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001398 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001399 fmt.Printf("Error: %s\n", err.Error())
1400 return err
1401 }
1402 return err
1403 }
gio7fbd4ad2024-08-27 10:06:39 +04001404 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001405 fmt.Printf("Error: %s\n", err.Error())
1406 return err
1407 }
1408 return nil
1409}
1410
gio11617ac2024-07-15 16:09:04 +04001411func pickNetwork(networks []installer.Network, network string) []installer.Network {
1412 for _, n := range networks {
1413 if n.Name == network {
1414 return []installer.Network{n}
1415 }
1416 }
1417 return []installer.Network{}
1418}
1419
1420type NetworkFilter interface {
1421 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1422}
1423
1424type noNetworkFilter struct{}
1425
1426func NewNoNetworkFilter() NetworkFilter {
1427 return noNetworkFilter{}
1428}
1429
gio8fae3af2024-07-25 13:43:31 +04001430func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001431 return networks, nil
1432}
1433
1434type filterByOwner struct {
1435 st Store
1436}
1437
1438func NewNetworkFilterByOwner(st Store) NetworkFilter {
1439 return &filterByOwner{st}
1440}
1441
1442func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001443 if user == "" {
1444 return networks, nil
1445 }
gio11617ac2024-07-15 16:09:04 +04001446 network, err := f.st.GetUserNetwork(user)
1447 if err != nil {
1448 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001449 }
1450 ret := []installer.Network{}
1451 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001452 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001453 ret = append(ret, n)
1454 }
1455 }
giocb34ad22024-07-11 08:01:13 +04001456 return ret, nil
1457}
gio11617ac2024-07-15 16:09:04 +04001458
1459type allowListFilter struct {
1460 allowed []string
1461}
1462
1463func NewAllowListFilter(allowed []string) NetworkFilter {
1464 return &allowListFilter{allowed}
1465}
1466
gio8fae3af2024-07-25 13:43:31 +04001467func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001468 ret := []installer.Network{}
1469 for _, n := range networks {
1470 if slices.Contains(f.allowed, n.Name) {
1471 ret = append(ret, n)
1472 }
1473 }
1474 return ret, nil
1475}
1476
1477type combinedNetworkFilter struct {
1478 filters []NetworkFilter
1479}
1480
1481func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1482 return &combinedNetworkFilter{filters}
1483}
1484
gio8fae3af2024-07-25 13:43:31 +04001485func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001486 ret := networks
1487 var err error
1488 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001489 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001490 if err != nil {
1491 return nil, err
1492 }
1493 }
1494 return ret, nil
1495}
giocafd4e62024-07-31 10:53:40 +04001496
1497type user struct {
1498 Username string `json:"username"`
1499 Email string `json:"email"`
1500 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1501}
1502
1503func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1504 go s.syncUsers()
1505}
1506
1507func (s *DodoAppServer) syncUsers() {
1508 if s.external {
1509 panic("MUST NOT REACH!")
1510 }
1511 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1512 if err != nil {
1513 return
1514 }
1515 users := []user{}
1516 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1517 fmt.Println(err)
1518 return
1519 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001520 validUsernames := make(map[string]user)
1521 for _, u := range users {
1522 validUsernames[u.Username] = u
1523 }
1524 allClientUsers, err := s.client.GetAllUsers()
1525 if err != nil {
1526 fmt.Println(err)
1527 return
1528 }
1529 keyToUser := make(map[string]string)
1530 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001531 if clientUser == "admin" || clientUser == "fluxcd" {
1532 continue
1533 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001534 userData, ok := validUsernames[clientUser]
1535 if !ok {
1536 if err := s.client.RemoveUser(clientUser); err != nil {
1537 fmt.Println(err)
1538 return
1539 }
1540 } else {
1541 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1542 if err != nil {
1543 fmt.Println(err)
1544 return
1545 }
1546 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001547 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001548 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001549 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001550 })
1551 if !keyOk {
1552 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1553 fmt.Println(err)
1554 }
1555 } else {
1556 keyToUser[cleanKey] = clientUser
1557 }
1558 }
1559 }
1560 }
giocafd4e62024-07-31 10:53:40 +04001561 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001562 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1563 fmt.Println(err)
1564 return
1565 }
giocafd4e62024-07-31 10:53:40 +04001566 if len(u.SSHPublicKeys) == 0 {
1567 continue
1568 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001569 ok, err := s.client.UserExists(u.Username)
1570 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001571 fmt.Println(err)
1572 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001573 }
1574 if !ok {
1575 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1576 fmt.Println(err)
1577 return
1578 }
1579 } else {
1580 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001581 cleanKey := soft.CleanKey(key)
1582 if user, ok := keyToUser[cleanKey]; ok {
1583 if u.Username != user {
1584 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1585 }
1586 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001587 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001588 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001589 fmt.Println(err)
1590 return
giocafd4e62024-07-31 10:53:40 +04001591 }
1592 }
1593 }
1594 }
1595 repos, err := s.client.GetAllRepos()
1596 if err != nil {
1597 return
1598 }
1599 for _, r := range repos {
1600 if r == ConfigRepoName {
1601 continue
1602 }
1603 for _, u := range users {
1604 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1605 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001606 continue
giocafd4e62024-07-31 10:53:40 +04001607 }
1608 }
1609 }
1610}
giob4a3a192024-08-19 09:55:47 +04001611
1612func extractResourceData(resources []installer.Resource) (resourceData, error) {
1613 var ret resourceData
1614 for _, r := range resources {
1615 t, ok := r.Annotations["dodo.cloud/resource-type"]
1616 if !ok {
1617 continue
1618 }
1619 switch t {
1620 case "volume":
1621 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1622 if !ok {
1623 return resourceData{}, fmt.Errorf("no name")
1624 }
1625 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1626 if !ok {
1627 return resourceData{}, fmt.Errorf("no size")
1628 }
1629 ret.Volume = append(ret.Volume, volume{name, size})
1630 case "postgresql":
1631 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1632 if !ok {
1633 return resourceData{}, fmt.Errorf("no name")
1634 }
1635 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1636 if !ok {
1637 return resourceData{}, fmt.Errorf("no version")
1638 }
1639 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1640 if !ok {
1641 return resourceData{}, fmt.Errorf("no volume")
1642 }
1643 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1644 case "ingress":
1645 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1646 if !ok {
1647 return resourceData{}, fmt.Errorf("no host")
1648 }
1649 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001650 case "virtual-machine":
1651 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1652 if !ok {
1653 return resourceData{}, fmt.Errorf("no name")
1654 }
1655 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1656 if !ok {
1657 return resourceData{}, fmt.Errorf("no user")
1658 }
1659 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1660 if !ok {
1661 return resourceData{}, fmt.Errorf("no cpu cores")
1662 }
1663 cpuCores, err := strconv.Atoi(cpuCoresS)
1664 if err != nil {
1665 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1666 }
1667 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1668 if !ok {
1669 return resourceData{}, fmt.Errorf("no memory")
1670 }
1671 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001672 default:
1673 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1674 }
1675 }
1676 return ret, nil
1677}
gio7fbd4ad2024-08-27 10:06:39 +04001678
1679func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
1680 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
1681 if err != nil {
1682 return "", nil, err
1683 }
1684 if err := cfg.Err(); err != nil {
1685 return "", nil, err
1686 }
1687 if err := cfg.Validate(); err != nil {
1688 return "", nil, err
1689 }
1690 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1691 if err := subdomain.Err(); err != nil {
1692 return "", nil, err
1693 }
1694 subdomainStr, err := subdomain.String()
1695 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1696 if err := network.Err(); err != nil {
1697 return "", nil, err
1698 }
1699 networkStr, err := network.String()
1700 if err != nil {
1701 return "", nil, err
1702 }
1703 newCfg := map[string]any{}
1704 if err := cfg.Decode(&newCfg); err != nil {
1705 return "", nil, err
1706 }
1707 app, ok := newCfg["app"].(map[string]any)
1708 if !ok {
1709 return "", nil, fmt.Errorf("not a map")
1710 }
1711 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1712 app["dev"] = map[string]any{
1713 "enabled": true,
1714 "username": username,
1715 }
1716 buf, err := json.MarshalIndent(newCfg, "", "\t")
1717 if err != nil {
1718 return "", nil, err
1719 }
1720 return networkStr, buf, nil
1721}