blob: 7f3d383d9ffd0f6b061155616c3d365fed21ecf9 [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
giocb34ad22024-07-11 08:01:13 +0400107 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400108 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +0400109 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +0400110 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400111 external bool
112 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400113 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400114 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400115}
116
117type appConfig struct {
118 Namespace string `json:"namespace"`
119 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400120}
121
gio33059762024-07-05 13:19:07 +0400122// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400123func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400124 st Store,
gio11617ac2024-07-15 16:09:04 +0400125 nf NetworkFilter,
126 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400127 port int,
gioa60f0de2024-07-08 10:49:48 +0400128 apiPort int,
gio33059762024-07-05 13:19:07 +0400129 self string,
gio11617ac2024-07-15 16:09:04 +0400130 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400131 sshKey string,
gio33059762024-07-05 13:19:07 +0400132 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400133 client soft.Client,
134 namespace string,
giocb34ad22024-07-11 08:01:13 +0400135 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400136 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400137 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400138 vpnKeyGen installer.VPNAPIClient,
gio0eaf2712024-04-14 13:08:46 +0400139 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400140 external bool,
141 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400142 reconciler tasks.Reconciler,
gio9d66f322024-07-06 13:45:10 +0400143) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400144 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
145 if err != nil {
146 return nil, err
147 }
gio5e49bb62024-07-20 10:43:19 +0400148 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
149 if err != nil {
150 return nil, err
151 }
152 appTmpls, err := NewAppTmplStoreFS(apps)
153 if err != nil {
154 return nil, err
155 }
gio9d66f322024-07-06 13:45:10 +0400156 s := &DodoAppServer{
157 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400158 st,
gio11617ac2024-07-15 16:09:04 +0400159 nf,
160 ug,
gio0eaf2712024-04-14 13:08:46 +0400161 port,
gioa60f0de2024-07-08 10:49:48 +0400162 apiPort,
gio33059762024-07-05 13:19:07 +0400163 self,
gio11617ac2024-07-15 16:09:04 +0400164 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400165 sshKey,
gio33059762024-07-05 13:19:07 +0400166 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400167 client,
168 namespace,
giocb34ad22024-07-11 08:01:13 +0400169 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400170 env,
gio33059762024-07-05 13:19:07 +0400171 nsc,
giof8843412024-05-22 16:38:05 +0400172 jc,
gio36b23b32024-08-25 12:20:54 +0400173 vpnKeyGen,
gio266c04f2024-07-03 14:18:45 +0400174 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400175 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400176 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400177 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400178 external,
179 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400180 reconciler,
gio183e8342024-08-20 06:01:24 +0400181 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400182 }
gioa60f0de2024-07-08 10:49:48 +0400183 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400184 if err != nil {
185 return nil, err
186 }
giod8ab4f52024-07-26 16:58:34 +0400187 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400188 if err == nil {
189 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400190 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400191 return nil, err
192 }
193 } else if !errors.Is(err, fs.ErrNotExist) {
194 return nil, err
195 }
196 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400197}
198
gio7fbd4ad2024-08-27 10:06:39 +0400199func (s *DodoAppServer) getAppConfig(app, branch string) appConfig {
200 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
201}
202
203func (s *DodoAppServer) setAppConfig(app, branch string, cfg appConfig) {
204 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
205}
206
gio0eaf2712024-04-14 13:08:46 +0400207func (s *DodoAppServer) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400208 // if err := s.client.DisableKeyless(); err != nil {
209 // return err
210 // }
211 // if err := s.client.DisableAnonAccess(); err != nil {
212 // return err
213 // }
gioa60f0de2024-07-08 10:49:48 +0400214 e := make(chan error)
215 go func() {
216 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400217 r.Use(s.mwAuth)
gio1bf00802024-08-17 12:31:41 +0400218 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400219 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400220 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
221 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400222 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
223 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400224 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400225 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400226 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
227 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio81246f02024-07-10 12:02:15 +0400228 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
229 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400230 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400231 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
232 }()
233 go func() {
234 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400235 r.HandleFunc("/update", s.handleAPIUpdate)
236 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400237 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400238 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400239 if !s.external {
240 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
241 }
gioa60f0de2024-07-08 10:49:48 +0400242 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
243 }()
giocafd4e62024-07-31 10:53:40 +0400244 if !s.external {
245 go func() {
246 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400247 for {
248 delay := time.Duration(rand.Intn(60)+60) * time.Second
249 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400250 s.syncUsers()
251 }
252 }()
253 }
gioa60f0de2024-07-08 10:49:48 +0400254 return <-e
255}
256
gio11617ac2024-07-15 16:09:04 +0400257type UserGetter interface {
258 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400259 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400260}
261
262type externalUserGetter struct {
263 sc *securecookie.SecureCookie
264}
265
266func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400267 return &externalUserGetter{securecookie.New(
268 securecookie.GenerateRandomKey(64),
269 securecookie.GenerateRandomKey(32),
270 )}
gio11617ac2024-07-15 16:09:04 +0400271}
272
273func (ug *externalUserGetter) Get(r *http.Request) string {
274 cookie, err := r.Cookie(sessionCookie)
275 if err != nil {
276 return ""
277 }
278 var user string
279 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
280 return ""
281 }
282 return user
283}
284
gio8fae3af2024-07-25 13:43:31 +0400285func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
286 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
287 cookie := &http.Cookie{
288 Name: sessionCookie,
289 Value: encoded,
290 Path: "/",
291 Secure: true,
292 HttpOnly: true,
293 }
294 http.SetCookie(w, cookie)
295 return nil
296 } else {
297 return err
298 }
299}
300
gio11617ac2024-07-15 16:09:04 +0400301type internalUserGetter struct{}
302
303func NewInternalUserGetter() UserGetter {
304 return internalUserGetter{}
305}
306
307func (ug internalUserGetter) Get(r *http.Request) string {
308 return r.Header.Get("X-User")
309}
310
gio8fae3af2024-07-25 13:43:31 +0400311func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
312 return nil
313}
314
gio81246f02024-07-10 12:02:15 +0400315func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
316 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400317 if strings.HasSuffix(r.URL.Path, loginPath) ||
318 strings.HasPrefix(r.URL.Path, logoutPath) ||
319 strings.HasPrefix(r.URL.Path, staticPath) ||
320 strings.HasPrefix(r.URL.Path, apiPublicData) ||
321 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400322 next.ServeHTTP(w, r)
323 return
324 }
gio11617ac2024-07-15 16:09:04 +0400325 user := s.ug.Get(r)
326 if user == "" {
gio81246f02024-07-10 12:02:15 +0400327 vars := mux.Vars(r)
328 appName, ok := vars["app-name"]
329 if !ok || appName == "" {
330 http.Error(w, "missing app-name", http.StatusBadRequest)
331 return
332 }
333 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
334 return
335 }
gio81246f02024-07-10 12:02:15 +0400336 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
337 })
338}
339
340func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400341 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400342 http.SetCookie(w, &http.Cookie{
343 Name: sessionCookie,
344 Value: "",
345 Path: "/",
346 HttpOnly: true,
347 Secure: true,
348 })
349 http.Redirect(w, r, "/", http.StatusSeeOther)
350}
351
352func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
353 vars := mux.Vars(r)
354 appName, ok := vars["app-name"]
355 if !ok || appName == "" {
356 http.Error(w, "missing app-name", http.StatusBadRequest)
357 return
358 }
359 fmt.Fprint(w, `
360<!DOCTYPE html>
361<html lang='en'>
362 <head>
363 <title>dodo: app - login</title>
364 <meta charset='utf-8'>
365 </head>
366 <body>
367 <form action="" method="POST">
368 <input type="password" placeholder="Password" name="password" required />
369 <button type="submit">Login</button>
370 </form>
371 </body>
372</html>
373`)
374}
375
376func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
377 vars := mux.Vars(r)
378 appName, ok := vars["app-name"]
379 if !ok || appName == "" {
380 http.Error(w, "missing app-name", http.StatusBadRequest)
381 return
382 }
383 password := r.FormValue("password")
384 if password == "" {
385 http.Error(w, "missing password", http.StatusBadRequest)
386 return
387 }
388 user, err := s.st.GetAppOwner(appName)
389 if err != nil {
390 http.Error(w, err.Error(), http.StatusInternalServerError)
391 return
392 }
393 hashed, err := s.st.GetUserPassword(user)
394 if err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
397 }
398 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
399 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
400 return
401 }
gio8fae3af2024-07-25 13:43:31 +0400402 if err := s.ug.Encode(w, user); err != nil {
403 http.Error(w, err.Error(), http.StatusInternalServerError)
404 return
gio81246f02024-07-10 12:02:15 +0400405 }
406 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
407}
408
giob4a3a192024-08-19 09:55:47 +0400409type navItem struct {
410 Name string
411 Address string
412}
413
gio23bdc1b2024-07-11 16:07:47 +0400414type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400415 Navigation []navItem
416 Apps []string
417 Networks []installer.Network
418 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400419}
420
gioa60f0de2024-07-08 10:49:48 +0400421func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400422 user := r.Context().Value(userCtx)
423 if user == nil {
424 http.Error(w, "unauthorized", http.StatusUnauthorized)
425 return
426 }
427 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400428 if err != nil {
429 http.Error(w, err.Error(), http.StatusInternalServerError)
430 return
431 }
gio11617ac2024-07-15 16:09:04 +0400432 networks, err := s.getNetworks(user.(string))
433 if err != nil {
434 http.Error(w, err.Error(), http.StatusInternalServerError)
435 return
436 }
giob54db242024-07-30 18:49:33 +0400437 var types []string
438 for _, t := range s.appTmpls.Types() {
439 types = append(types, strings.Replace(t, "-", ":", 1))
440 }
giob4a3a192024-08-19 09:55:47 +0400441 n := []navItem{navItem{"Home", "/"}}
442 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400443 if err := s.tmplts.index.Execute(w, data); err != nil {
444 http.Error(w, err.Error(), http.StatusInternalServerError)
445 return
gioa60f0de2024-07-08 10:49:48 +0400446 }
447}
448
gio5e49bb62024-07-20 10:43:19 +0400449type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400450 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400451 Name string
452 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400453 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400454 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400455 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400456}
457
gioa60f0de2024-07-08 10:49:48 +0400458func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
459 vars := mux.Vars(r)
460 appName, ok := vars["app-name"]
461 if !ok || appName == "" {
462 http.Error(w, "missing app-name", http.StatusBadRequest)
463 return
464 }
gio7fbd4ad2024-08-27 10:06:39 +0400465 branch, ok := vars["branch"]
466 if !ok || branch == "" {
467 branch = "master"
468 }
gio94904702024-07-26 16:58:34 +0400469 u := r.Context().Value(userCtx)
470 if u == nil {
471 http.Error(w, "unauthorized", http.StatusUnauthorized)
472 return
473 }
474 user, ok := u.(string)
475 if !ok {
476 http.Error(w, "could not get user", http.StatusInternalServerError)
477 return
478 }
479 owner, err := s.st.GetAppOwner(appName)
480 if err != nil {
481 http.Error(w, err.Error(), http.StatusInternalServerError)
482 return
483 }
484 if owner != user {
485 http.Error(w, "unauthorized", http.StatusUnauthorized)
486 return
487 }
gio7fbd4ad2024-08-27 10:06:39 +0400488 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400489 if err != nil {
490 http.Error(w, err.Error(), http.StatusInternalServerError)
491 return
492 }
gio183e8342024-08-20 06:01:24 +0400493 var lastCommitResources resourceData
494 if len(commits) > 0 {
495 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
496 if err != nil {
497 http.Error(w, err.Error(), http.StatusInternalServerError)
498 return
499 }
500 r, err := extractResourceData(lastCommit.Resources.Helm)
501 if err != nil {
502 http.Error(w, err.Error(), http.StatusInternalServerError)
503 return
504 }
505 lastCommitResources = r
506 }
gio7fbd4ad2024-08-27 10:06:39 +0400507 branches, err := s.st.GetBranches(appName)
508 if err != nil {
509 http.Error(w, err.Error(), http.StatusInternalServerError)
510 return
511 }
gio5e49bb62024-07-20 10:43:19 +0400512 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400513 Navigation: []navItem{
514 navItem{"Home", "/"},
515 navItem{appName, "/" + appName},
516 },
gio5e49bb62024-07-20 10:43:19 +0400517 Name: appName,
518 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
519 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400520 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400521 Branches: branches,
522 }
523 if branch != "master" {
524 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400525 }
526 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
gioa60f0de2024-07-08 10:49:48 +0400529 }
gio0eaf2712024-04-14 13:08:46 +0400530}
531
giocfb228c2024-09-06 15:44:31 +0400532type appEnv struct {
533 Profile string `json:"envProfile"`
534}
535
536func (s *DodoAppServer) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
537 vars := mux.Vars(r)
538 appName, ok := vars["app-name"]
539 if !ok || appName == "" {
540 http.Error(w, "missing app-name", http.StatusBadRequest)
541 return
542 }
543 branch, ok := vars["branch"]
544 if !ok || branch == "" {
545 branch = "master"
546 }
547 info, err := s.st.GetLastCommitInfo(appName, branch)
548 if err != nil {
549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
551 }
552 var e appEnv
553 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
554 http.Error(w, err.Error(), http.StatusInternalServerError)
555 return
556 }
557 fmt.Fprintln(w, e.Profile)
558}
559
giob4a3a192024-08-19 09:55:47 +0400560type volume struct {
561 Name string
562 Size string
563}
564
565type postgresql struct {
566 Name string
567 Version string
568 Volume string
569}
570
571type ingress struct {
572 Host string
573}
574
gio7fbd4ad2024-08-27 10:06:39 +0400575type vm struct {
576 Name string
577 User string
578 CPUCores int
579 Memory string
580}
581
giob4a3a192024-08-19 09:55:47 +0400582type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400583 Volume []volume
584 PostgreSQL []postgresql
585 Ingress []ingress
586 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400587}
588
589type commitStatusData struct {
590 Navigation []navItem
591 AppName string
592 Commit Commit
593 Resources resourceData
594}
595
596func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
597 vars := mux.Vars(r)
598 appName, ok := vars["app-name"]
599 if !ok || appName == "" {
600 http.Error(w, "missing app-name", http.StatusBadRequest)
601 return
602 }
603 hash, ok := vars["hash"]
604 if !ok || appName == "" {
605 http.Error(w, "missing app-name", http.StatusBadRequest)
606 return
607 }
608 u := r.Context().Value(userCtx)
609 if u == nil {
610 http.Error(w, "unauthorized", http.StatusUnauthorized)
611 return
612 }
613 user, ok := u.(string)
614 if !ok {
615 http.Error(w, "could not get user", http.StatusInternalServerError)
616 return
617 }
618 owner, err := s.st.GetAppOwner(appName)
619 if err != nil {
620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
622 }
623 if owner != user {
624 http.Error(w, "unauthorized", http.StatusUnauthorized)
625 return
626 }
627 commit, err := s.st.GetCommit(hash)
628 if err != nil {
629 // TODO(gio): not-found ?
630 http.Error(w, err.Error(), http.StatusInternalServerError)
631 return
632 }
633 var res strings.Builder
634 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
637 }
638 resData, err := extractResourceData(commit.Resources.Helm)
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
643 data := commitStatusData{
644 Navigation: []navItem{
645 navItem{"Home", "/"},
646 navItem{appName, "/" + appName},
647 navItem{hash, "/" + appName + "/" + hash},
648 },
649 AppName: appName,
650 Commit: commit,
651 Resources: resData,
652 }
653 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
654 http.Error(w, err.Error(), http.StatusInternalServerError)
655 return
656 }
657}
658
gio183e8342024-08-20 06:01:24 +0400659type logData struct {
660 Navigation []navItem
661 AppName string
662 Logs template.HTML
663}
664
665func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
666 vars := mux.Vars(r)
667 appName, ok := vars["app-name"]
668 if !ok || appName == "" {
669 http.Error(w, "missing app-name", http.StatusBadRequest)
670 return
671 }
672 u := r.Context().Value(userCtx)
673 if u == nil {
674 http.Error(w, "unauthorized", http.StatusUnauthorized)
675 return
676 }
677 user, ok := u.(string)
678 if !ok {
679 http.Error(w, "could not get user", http.StatusInternalServerError)
680 return
681 }
682 owner, err := s.st.GetAppOwner(appName)
683 if err != nil {
684 http.Error(w, err.Error(), http.StatusInternalServerError)
685 return
686 }
687 if owner != user {
688 http.Error(w, "unauthorized", http.StatusUnauthorized)
689 return
690 }
691 data := logData{
692 Navigation: []navItem{
693 navItem{"Home", "/"},
694 navItem{appName, "/" + appName},
695 navItem{"Logs", "/" + appName + "/logs"},
696 },
697 AppName: appName,
698 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
699 }
700 if err := s.tmplts.logs.Execute(w, data); err != nil {
701 fmt.Println(err)
702 http.Error(w, err.Error(), http.StatusInternalServerError)
703 return
704 }
705}
706
gio81246f02024-07-10 12:02:15 +0400707type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400708 Ref string `json:"ref"`
709 Repository struct {
710 Name string `json:"name"`
711 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400712 After string `json:"after"`
713 Commits []struct {
714 Id string `json:"id"`
715 Message string `json:"message"`
716 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400717}
718
gio8fae3af2024-07-25 13:43:31 +0400719func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400720 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400721 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400722 var contents strings.Builder
723 io.Copy(&contents, r.Body)
724 c := contents.String()
725 fmt.Println(c)
726 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400727 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400728 return
729 }
gio7fbd4ad2024-08-27 10:06:39 +0400730 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
731 return
732 }
733 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
734 if !ok {
735 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400736 return
737 }
gioa60f0de2024-07-08 10:49:48 +0400738 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400739 go func() {
gio11617ac2024-07-15 16:09:04 +0400740 owner, err := s.st.GetAppOwner(req.Repository.Name)
741 if err != nil {
742 return
743 }
744 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400745 if err != nil {
746 return
747 }
gio94904702024-07-26 16:58:34 +0400748 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
749 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
750 if err != nil {
751 return
752 }
gioe2e31e12024-08-18 08:20:56 +0400753 found := false
754 commitMsg := ""
755 for _, c := range req.Commits {
756 if c.Id == req.After {
757 found = true
758 commitMsg = c.Message
759 break
gioa60f0de2024-07-08 10:49:48 +0400760 }
761 }
gioe2e31e12024-08-18 08:20:56 +0400762 if !found {
763 fmt.Printf("Error: could not find commit message")
764 return
765 }
gio7fbd4ad2024-08-27 10:06:39 +0400766 s.l.Lock()
767 defer s.l.Unlock()
768 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, owner)
769 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400770 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400771 return
772 }
gioa60f0de2024-07-08 10:49:48 +0400773 for addr, _ := range s.workers[req.Repository.Name] {
774 go func() {
775 // TODO(gio): make port configurable
776 http.Get(fmt.Sprintf("http://%s/update", addr))
777 }()
gio0eaf2712024-04-14 13:08:46 +0400778 }
779 }()
gio0eaf2712024-04-14 13:08:46 +0400780}
781
gio81246f02024-07-10 12:02:15 +0400782type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400783 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400784 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400785}
786
gio8fae3af2024-07-25 13:43:31 +0400787func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400788 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400789 vars := mux.Vars(r)
790 appName, ok := vars["app-name"]
791 if !ok || appName == "" {
792 http.Error(w, "missing app-name", http.StatusBadRequest)
793 return
794 }
gio81246f02024-07-10 12:02:15 +0400795 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400796 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
797 http.Error(w, err.Error(), http.StatusInternalServerError)
798 return
799 }
gioa60f0de2024-07-08 10:49:48 +0400800 if _, ok := s.workers[appName]; !ok {
801 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400802 }
gioa60f0de2024-07-08 10:49:48 +0400803 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400804 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400805}
806
gio11617ac2024-07-15 16:09:04 +0400807func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
808 u := r.Context().Value(userCtx)
809 if u == nil {
810 http.Error(w, "unauthorized", http.StatusUnauthorized)
811 return
812 }
813 user, ok := u.(string)
814 if !ok {
815 http.Error(w, "could not get user", http.StatusInternalServerError)
816 return
817 }
818 network := r.FormValue("network")
819 if network == "" {
820 http.Error(w, "missing network", http.StatusBadRequest)
821 return
822 }
gio5e49bb62024-07-20 10:43:19 +0400823 subdomain := r.FormValue("subdomain")
824 if subdomain == "" {
825 http.Error(w, "missing subdomain", http.StatusBadRequest)
826 return
827 }
828 appType := r.FormValue("type")
829 if appType == "" {
830 http.Error(w, "missing type", http.StatusBadRequest)
831 return
832 }
gio11617ac2024-07-15 16:09:04 +0400833 g := installer.NewFixedLengthRandomNameGenerator(3)
834 appName, err := g.Generate()
835 if err != nil {
836 http.Error(w, err.Error(), http.StatusInternalServerError)
837 return
838 }
839 if ok, err := s.client.UserExists(user); err != nil {
840 http.Error(w, err.Error(), http.StatusInternalServerError)
841 return
842 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400843 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
844 return
gio11617ac2024-07-15 16:09:04 +0400845 }
giocafd4e62024-07-31 10:53:40 +0400846 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400847 http.Error(w, err.Error(), http.StatusInternalServerError)
848 return
849 }
850 if err := s.st.CreateApp(appName, user); err != nil {
851 http.Error(w, err.Error(), http.StatusInternalServerError)
852 return
853 }
giod8ab4f52024-07-26 16:58:34 +0400854 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400855 http.Error(w, err.Error(), http.StatusInternalServerError)
856 return
857 }
858 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
859}
860
gio7fbd4ad2024-08-27 10:06:39 +0400861func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
862 u := r.Context().Value(userCtx)
863 if u == nil {
864 http.Error(w, "unauthorized", http.StatusUnauthorized)
865 return
866 }
867 user, ok := u.(string)
868 if !ok {
869 http.Error(w, "could not get user", http.StatusInternalServerError)
870 return
871 }
872 vars := mux.Vars(r)
873 appName, ok := vars["app-name"]
874 if !ok || appName == "" {
875 http.Error(w, "missing app-name", http.StatusBadRequest)
876 return
877 }
878 branch := r.FormValue("branch")
879 if branch == "" {
880 http.Error(w, "missing network", http.StatusBadRequest)
881 return
882 }
883 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
884 http.Error(w, err.Error(), http.StatusInternalServerError)
885 return
886 }
887 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
888}
889
gio81246f02024-07-10 12:02:15 +0400890type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400891 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400892 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400893 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400894 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400895}
896
gio81246f02024-07-10 12:02:15 +0400897type apiCreateAppResp struct {
898 AppName string `json:"appName"`
899 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400900}
901
gio8fae3af2024-07-25 13:43:31 +0400902func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400903 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400904 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400905 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
906 http.Error(w, err.Error(), http.StatusBadRequest)
907 return
908 }
909 g := installer.NewFixedLengthRandomNameGenerator(3)
910 appName, err := g.Generate()
911 if err != nil {
912 http.Error(w, err.Error(), http.StatusInternalServerError)
913 return
914 }
gio11617ac2024-07-15 16:09:04 +0400915 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400916 if err != nil {
gio33059762024-07-05 13:19:07 +0400917 http.Error(w, err.Error(), http.StatusInternalServerError)
918 return
919 }
gio11617ac2024-07-15 16:09:04 +0400920 if user != "" {
921 http.Error(w, "public key already registered", http.StatusBadRequest)
922 return
923 }
924 user = appName
925 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
926 http.Error(w, err.Error(), http.StatusInternalServerError)
927 return
928 }
929 password := generatePassword()
930 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
931 if err != nil {
932 http.Error(w, err.Error(), http.StatusInternalServerError)
933 return
934 }
giocafd4e62024-07-31 10:53:40 +0400935 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400936 http.Error(w, err.Error(), http.StatusInternalServerError)
937 return
938 }
939 if err := s.st.CreateApp(appName, user); err != nil {
940 http.Error(w, err.Error(), http.StatusInternalServerError)
941 return
942 }
giod8ab4f52024-07-26 16:58:34 +0400943 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400944 http.Error(w, err.Error(), http.StatusInternalServerError)
945 return
946 }
gio81246f02024-07-10 12:02:15 +0400947 resp := apiCreateAppResp{
948 AppName: appName,
949 Password: password,
950 }
gio33059762024-07-05 13:19:07 +0400951 if err := json.NewEncoder(w).Encode(resp); err != nil {
952 http.Error(w, err.Error(), http.StatusInternalServerError)
953 return
954 }
955}
956
giod8ab4f52024-07-26 16:58:34 +0400957func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400958 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400959 return true
960 }
961 for _, cfg := range s.appConfigs {
962 if strings.ToLower(cfg.Network) == network {
963 return false
964 }
965 }
966 return true
967}
968
969func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400970 s.l.Lock()
971 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400972 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400973 network = strings.ToLower(network)
974 if !s.isNetworkUseAllowed(network) {
975 return fmt.Errorf("network already used: %s", network)
976 }
gio33059762024-07-05 13:19:07 +0400977 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400978 return err
gio33059762024-07-05 13:19:07 +0400979 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400980 return nil
gioa60f0de2024-07-08 10:49:48 +0400981 }
gio5e49bb62024-07-20 10:43:19 +0400982 networks, err := s.getNetworks(user)
983 if err != nil {
984 return err
985 }
giod8ab4f52024-07-26 16:58:34 +0400986 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400987 if !ok {
988 return fmt.Errorf("network not found: %s\n", network)
989 }
gio33059762024-07-05 13:19:07 +0400990 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400991 return err
gio33059762024-07-05 13:19:07 +0400992 }
993 appRepo, err := s.client.GetRepo(appName)
994 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400995 return err
gio33059762024-07-05 13:19:07 +0400996 }
gio7fbd4ad2024-08-27 10:06:39 +0400997 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
998 if err != nil {
999 return err
1000 }
1001 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1002}
1003
1004func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
1005 s.l.Lock()
1006 defer s.l.Unlock()
1007 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1008 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1009 if err != nil {
1010 return err
1011 }
1012 appCfg, err := soft.ReadFile(appRepo, "app.cue")
1013 if err != nil {
1014 return err
1015 }
1016 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1017 if err != nil {
1018 return err
1019 }
1020 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
1021}
1022
1023func (s *DodoAppServer) createAppForBranch(
1024 repo soft.RepoIO,
1025 appName string,
1026 branch string,
1027 user string,
1028 network string,
1029 files map[string][]byte,
1030) error {
1031 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1032 for path, contents := range files {
1033 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1034 return "", err
1035 }
1036 }
1037 return "init", nil
1038 }, soft.WithCommitToBranch(branch))
1039 if err != nil {
1040 return err
1041 }
1042 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001043 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001044 return err
gio33059762024-07-05 13:19:07 +04001045 }
1046 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001047 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1048 if err != nil {
1049 return err
1050 }
1051 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001052 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001053 return err
gio33059762024-07-05 13:19:07 +04001054 }
1055 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1056 suffix, err := suffixGen.Generate()
1057 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001058 return err
gio33059762024-07-05 13:19:07 +04001059 }
gio94904702024-07-26 16:58:34 +04001060 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001061 s.setAppConfig(appName, branch, appConfig{namespace, network})
1062 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, user)
giob4a3a192024-08-19 09:55:47 +04001063 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001064 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001065 return err
1066 }
gio7fbd4ad2024-08-27 10:06:39 +04001067 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001068 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001069 return err
gio33059762024-07-05 13:19:07 +04001070 }
giod8ab4f52024-07-26 16:58:34 +04001071 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001072 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001073 return err
gio33059762024-07-05 13:19:07 +04001074 }
1075 hf := installer.NewGitHelmFetcher()
gio36b23b32024-08-25 12:20:54 +04001076 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, "/")
gio33059762024-07-05 13:19:07 +04001077 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001078 return err
gio33059762024-07-05 13:19:07 +04001079 }
gio7fbd4ad2024-08-27 10:06:39 +04001080 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001081 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001082 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001083 if err != nil {
1084 return "", err
1085 }
1086 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001087 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001088 return "", err
1089 }
1090 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001091 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001092 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001093 appPath,
gio9d66f322024-07-06 13:45:10 +04001094 namespace,
1095 map[string]any{
1096 "repoAddr": s.client.GetRepoAddress(appName),
1097 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001098 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001099 "gitRepoPublicKey": s.gitRepoPublicKey,
1100 },
1101 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001102 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001103 installer.WithNoPublish(),
1104 installer.WithNoLock(),
1105 ); err != nil {
1106 return "", err
1107 }
1108 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001109 })
1110 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001111 return err
gio33059762024-07-05 13:19:07 +04001112 }
gio7fbd4ad2024-08-27 10:06:39 +04001113 return s.initAppACLs(m, appPath, appName, branch, user)
1114}
1115
1116func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
1117 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001118 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001119 return err
gio33059762024-07-05 13:19:07 +04001120 }
1121 fluxKeys, ok := cfg.Input["fluxKeys"]
1122 if !ok {
gio11617ac2024-07-15 16:09:04 +04001123 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001124 }
1125 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1126 if !ok {
gio11617ac2024-07-15 16:09:04 +04001127 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001128 }
1129 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001130 return err
gio33059762024-07-05 13:19:07 +04001131 } else if ok {
1132 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001133 return err
gio33059762024-07-05 13:19:07 +04001134 }
1135 } else {
1136 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001137 return err
gio33059762024-07-05 13:19:07 +04001138 }
1139 }
gio7fbd4ad2024-08-27 10:06:39 +04001140 if branch != "master" {
1141 return nil
1142 }
gio33059762024-07-05 13:19:07 +04001143 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001144 return err
gio33059762024-07-05 13:19:07 +04001145 }
gio7fbd4ad2024-08-27 10:06:39 +04001146 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001147 return err
gio33059762024-07-05 13:19:07 +04001148 }
gio7fbd4ad2024-08-27 10:06:39 +04001149 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 +04001150 return err
gio33059762024-07-05 13:19:07 +04001151 }
gio2ccb6e32024-08-15 12:01:33 +04001152 if !s.external {
1153 go func() {
1154 users, err := s.client.GetAllUsers()
1155 if err != nil {
1156 fmt.Println(err)
1157 return
1158 }
1159 for _, user := range users {
1160 // TODO(gio): fluxcd should have only read access
1161 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1162 fmt.Println(err)
1163 }
1164 }
1165 }()
1166 }
gio43b0f422024-08-21 10:40:13 +04001167 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1168 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001169 return nil
gio33059762024-07-05 13:19:07 +04001170}
1171
gio81246f02024-07-10 12:02:15 +04001172type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001173 User string `json:"user"`
1174 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001175}
1176
gio7fbd4ad2024-08-27 10:06:39 +04001177func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001178 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001179 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1180 http.Error(w, err.Error(), http.StatusBadRequest)
1181 return
1182 }
gio7fbd4ad2024-08-27 10:06:39 +04001183 if req.User == "" {
1184 http.Error(w, "invalid user", http.StatusBadRequest)
1185 return
1186 }
1187 if req.PublicKey == "" {
1188 http.Error(w, "invalid public key", http.StatusBadRequest)
1189 return
1190 }
1191 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001192 http.Error(w, err.Error(), http.StatusInternalServerError)
1193 return
1194 }
1195}
1196
gio94904702024-07-26 16:58:34 +04001197type dodoAppRendered struct {
1198 App struct {
1199 Ingress struct {
1200 Network string `json:"network"`
1201 Subdomain string `json:"subdomain"`
1202 } `json:"ingress"`
1203 } `json:"app"`
1204 Input struct {
1205 AppId string `json:"appId"`
1206 } `json:"input"`
1207}
1208
gio7fbd4ad2024-08-27 10:06:39 +04001209// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio43b0f422024-08-21 10:40:13 +04001210func (s *DodoAppServer) updateDodoApp(
1211 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001212 name string,
1213 branch string,
1214 namespace string,
gio43b0f422024-08-21 10:40:13 +04001215 networks []installer.Network,
gio7fbd4ad2024-08-27 10:06:39 +04001216 owner string,
gio43b0f422024-08-21 10:40:13 +04001217) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001218 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001219 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001220 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001221 }
giof8843412024-05-22 16:38:05 +04001222 hf := installer.NewGitHelmFetcher()
gio36b23b32024-08-25 12:20:54 +04001223 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001224 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001225 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001226 }
1227 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +04001228 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001229 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001230 }
1231 app, err := installer.NewDodoApp(appCfg)
1232 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001233 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001234 }
giof8843412024-05-22 16:38:05 +04001235 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001236 var ret installer.ReleaseResources
1237 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1238 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001239 app,
1240 "app",
1241 "/.dodo/app",
1242 namespace,
1243 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001244 "repoAddr": repo.FullAddress(),
1245 "repoPublicAddr": s.repoPublicAddr,
1246 "managerAddr": fmt.Sprintf("http://%s", s.self),
1247 "appId": name,
1248 "branch": branch,
1249 "sshPrivateKey": s.sshKey,
1250 "username": owner,
gio94904702024-07-26 16:58:34 +04001251 },
1252 installer.WithNoPull(),
1253 installer.WithNoPublish(),
1254 installer.WithConfig(&s.env),
1255 installer.WithNetworks(networks),
1256 installer.WithLocalChartGenerator(lg),
1257 installer.WithNoLock(),
1258 )
1259 if err != nil {
1260 return "", err
1261 }
1262 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001263 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001264 return "", nil
1265 }
1266 if _, err := m.Install(
1267 appStatus,
1268 "status",
1269 "/.dodo/status",
1270 s.namespace,
1271 map[string]any{
1272 "appName": rendered.Input.AppId,
1273 "network": rendered.App.Ingress.Network,
1274 "appSubdomain": rendered.App.Ingress.Subdomain,
1275 },
1276 installer.WithNoPull(),
1277 installer.WithNoPublish(),
1278 installer.WithConfig(&s.env),
1279 installer.WithNetworks(networks),
1280 installer.WithLocalChartGenerator(lg),
1281 installer.WithNoLock(),
1282 ); err != nil {
1283 return "", err
1284 }
1285 return "install app", nil
1286 },
gio7fbd4ad2024-08-27 10:06:39 +04001287 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001288 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001289 ); err != nil {
1290 return installer.ReleaseResources{}, err
1291 }
gio43b0f422024-08-21 10:40:13 +04001292 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1293 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001294 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001295}
gio33059762024-07-05 13:19:07 +04001296
gio7fbd4ad2024-08-27 10:06:39 +04001297func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001298 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001299 appTmpl, err := s.appTmpls.Find(appType)
1300 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001301 return nil, err
gio33059762024-07-05 13:19:07 +04001302 }
gio7fbd4ad2024-08-27 10:06:39 +04001303 return appTmpl.Render(network, subdomain)
gio33059762024-07-05 13:19:07 +04001304}
gio81246f02024-07-10 12:02:15 +04001305
1306func generatePassword() string {
1307 return "foo"
1308}
giocb34ad22024-07-11 08:01:13 +04001309
gio11617ac2024-07-15 16:09:04 +04001310func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001311 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001312 resp, err := http.Get(addr)
1313 if err != nil {
1314 return nil, err
1315 }
gio23bdc1b2024-07-11 16:07:47 +04001316 networks := []installer.Network{}
1317 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001318 return nil, err
1319 }
gio11617ac2024-07-15 16:09:04 +04001320 return s.nf.Filter(user, networks)
1321}
1322
gio8fae3af2024-07-25 13:43:31 +04001323type publicNetworkData struct {
1324 Name string `json:"name"`
1325 Domain string `json:"domain"`
1326}
1327
1328type publicData struct {
1329 Networks []publicNetworkData `json:"networks"`
1330 Types []string `json:"types"`
1331}
1332
1333func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001334 w.Header().Set("Access-Control-Allow-Origin", "*")
1335 s.l.Lock()
1336 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001337 networks, err := s.getNetworks("")
1338 if err != nil {
1339 http.Error(w, err.Error(), http.StatusInternalServerError)
1340 return
1341 }
1342 var ret publicData
1343 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001344 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1345 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1346 }
gio8fae3af2024-07-25 13:43:31 +04001347 }
1348 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001349 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001350 }
gio8fae3af2024-07-25 13:43:31 +04001351 if err := json.NewEncoder(w).Encode(ret); err != nil {
1352 http.Error(w, err.Error(), http.StatusInternalServerError)
1353 return
1354 }
1355}
1356
gio7fbd4ad2024-08-27 10:06:39 +04001357func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001358 if err != nil {
1359 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001360 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001361 fmt.Printf("Error: %s\n", err.Error())
1362 return err
1363 }
1364 return err
1365 }
1366 var resB bytes.Buffer
1367 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001368 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001369 fmt.Printf("Error: %s\n", err.Error())
1370 return err
1371 }
1372 return err
1373 }
gio7fbd4ad2024-08-27 10:06:39 +04001374 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001375 fmt.Printf("Error: %s\n", err.Error())
1376 return err
1377 }
1378 return nil
1379}
1380
gio11617ac2024-07-15 16:09:04 +04001381func pickNetwork(networks []installer.Network, network string) []installer.Network {
1382 for _, n := range networks {
1383 if n.Name == network {
1384 return []installer.Network{n}
1385 }
1386 }
1387 return []installer.Network{}
1388}
1389
1390type NetworkFilter interface {
1391 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1392}
1393
1394type noNetworkFilter struct{}
1395
1396func NewNoNetworkFilter() NetworkFilter {
1397 return noNetworkFilter{}
1398}
1399
gio8fae3af2024-07-25 13:43:31 +04001400func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001401 return networks, nil
1402}
1403
1404type filterByOwner struct {
1405 st Store
1406}
1407
1408func NewNetworkFilterByOwner(st Store) NetworkFilter {
1409 return &filterByOwner{st}
1410}
1411
1412func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001413 if user == "" {
1414 return networks, nil
1415 }
gio11617ac2024-07-15 16:09:04 +04001416 network, err := f.st.GetUserNetwork(user)
1417 if err != nil {
1418 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001419 }
1420 ret := []installer.Network{}
1421 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001422 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001423 ret = append(ret, n)
1424 }
1425 }
giocb34ad22024-07-11 08:01:13 +04001426 return ret, nil
1427}
gio11617ac2024-07-15 16:09:04 +04001428
1429type allowListFilter struct {
1430 allowed []string
1431}
1432
1433func NewAllowListFilter(allowed []string) NetworkFilter {
1434 return &allowListFilter{allowed}
1435}
1436
gio8fae3af2024-07-25 13:43:31 +04001437func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001438 ret := []installer.Network{}
1439 for _, n := range networks {
1440 if slices.Contains(f.allowed, n.Name) {
1441 ret = append(ret, n)
1442 }
1443 }
1444 return ret, nil
1445}
1446
1447type combinedNetworkFilter struct {
1448 filters []NetworkFilter
1449}
1450
1451func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1452 return &combinedNetworkFilter{filters}
1453}
1454
gio8fae3af2024-07-25 13:43:31 +04001455func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001456 ret := networks
1457 var err error
1458 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001459 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001460 if err != nil {
1461 return nil, err
1462 }
1463 }
1464 return ret, nil
1465}
giocafd4e62024-07-31 10:53:40 +04001466
1467type user struct {
1468 Username string `json:"username"`
1469 Email string `json:"email"`
1470 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1471}
1472
1473func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1474 go s.syncUsers()
1475}
1476
1477func (s *DodoAppServer) syncUsers() {
1478 if s.external {
1479 panic("MUST NOT REACH!")
1480 }
1481 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1482 if err != nil {
1483 return
1484 }
1485 users := []user{}
1486 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1487 fmt.Println(err)
1488 return
1489 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001490 validUsernames := make(map[string]user)
1491 for _, u := range users {
1492 validUsernames[u.Username] = u
1493 }
1494 allClientUsers, err := s.client.GetAllUsers()
1495 if err != nil {
1496 fmt.Println(err)
1497 return
1498 }
1499 keyToUser := make(map[string]string)
1500 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001501 if clientUser == "admin" || clientUser == "fluxcd" {
1502 continue
1503 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001504 userData, ok := validUsernames[clientUser]
1505 if !ok {
1506 if err := s.client.RemoveUser(clientUser); err != nil {
1507 fmt.Println(err)
1508 return
1509 }
1510 } else {
1511 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1512 if err != nil {
1513 fmt.Println(err)
1514 return
1515 }
1516 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001517 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001518 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001519 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001520 })
1521 if !keyOk {
1522 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1523 fmt.Println(err)
1524 }
1525 } else {
1526 keyToUser[cleanKey] = clientUser
1527 }
1528 }
1529 }
1530 }
giocafd4e62024-07-31 10:53:40 +04001531 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001532 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1533 fmt.Println(err)
1534 return
1535 }
giocafd4e62024-07-31 10:53:40 +04001536 if len(u.SSHPublicKeys) == 0 {
1537 continue
1538 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001539 ok, err := s.client.UserExists(u.Username)
1540 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001541 fmt.Println(err)
1542 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001543 }
1544 if !ok {
1545 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1546 fmt.Println(err)
1547 return
1548 }
1549 } else {
1550 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001551 cleanKey := soft.CleanKey(key)
1552 if user, ok := keyToUser[cleanKey]; ok {
1553 if u.Username != user {
1554 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1555 }
1556 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001557 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001558 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001559 fmt.Println(err)
1560 return
giocafd4e62024-07-31 10:53:40 +04001561 }
1562 }
1563 }
1564 }
1565 repos, err := s.client.GetAllRepos()
1566 if err != nil {
1567 return
1568 }
1569 for _, r := range repos {
1570 if r == ConfigRepoName {
1571 continue
1572 }
1573 for _, u := range users {
1574 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1575 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001576 continue
giocafd4e62024-07-31 10:53:40 +04001577 }
1578 }
1579 }
1580}
giob4a3a192024-08-19 09:55:47 +04001581
1582func extractResourceData(resources []installer.Resource) (resourceData, error) {
1583 var ret resourceData
1584 for _, r := range resources {
1585 t, ok := r.Annotations["dodo.cloud/resource-type"]
1586 if !ok {
1587 continue
1588 }
1589 switch t {
1590 case "volume":
1591 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1592 if !ok {
1593 return resourceData{}, fmt.Errorf("no name")
1594 }
1595 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1596 if !ok {
1597 return resourceData{}, fmt.Errorf("no size")
1598 }
1599 ret.Volume = append(ret.Volume, volume{name, size})
1600 case "postgresql":
1601 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1602 if !ok {
1603 return resourceData{}, fmt.Errorf("no name")
1604 }
1605 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1606 if !ok {
1607 return resourceData{}, fmt.Errorf("no version")
1608 }
1609 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1610 if !ok {
1611 return resourceData{}, fmt.Errorf("no volume")
1612 }
1613 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1614 case "ingress":
1615 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1616 if !ok {
1617 return resourceData{}, fmt.Errorf("no host")
1618 }
1619 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001620 case "virtual-machine":
1621 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1622 if !ok {
1623 return resourceData{}, fmt.Errorf("no name")
1624 }
1625 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1626 if !ok {
1627 return resourceData{}, fmt.Errorf("no user")
1628 }
1629 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1630 if !ok {
1631 return resourceData{}, fmt.Errorf("no cpu cores")
1632 }
1633 cpuCores, err := strconv.Atoi(cpuCoresS)
1634 if err != nil {
1635 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1636 }
1637 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1638 if !ok {
1639 return resourceData{}, fmt.Errorf("no memory")
1640 }
1641 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001642 default:
1643 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1644 }
1645 }
1646 return ret, nil
1647}
gio7fbd4ad2024-08-27 10:06:39 +04001648
1649func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
1650 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
1651 if err != nil {
1652 return "", nil, err
1653 }
1654 if err := cfg.Err(); err != nil {
1655 return "", nil, err
1656 }
1657 if err := cfg.Validate(); err != nil {
1658 return "", nil, err
1659 }
1660 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1661 if err := subdomain.Err(); err != nil {
1662 return "", nil, err
1663 }
1664 subdomainStr, err := subdomain.String()
1665 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1666 if err := network.Err(); err != nil {
1667 return "", nil, err
1668 }
1669 networkStr, err := network.String()
1670 if err != nil {
1671 return "", nil, err
1672 }
1673 newCfg := map[string]any{}
1674 if err := cfg.Decode(&newCfg); err != nil {
1675 return "", nil, err
1676 }
1677 app, ok := newCfg["app"].(map[string]any)
1678 if !ok {
1679 return "", nil, fmt.Errorf("not a map")
1680 }
1681 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1682 app["dev"] = map[string]any{
1683 "enabled": true,
1684 "username": username,
1685 }
1686 buf, err := json.MarshalIndent(newCfg, "", "\t")
1687 if err != nil {
1688 return "", nil, err
1689 }
1690 return networkStr, buf, nil
1691}