blob: 9f604497cb112866ef95cd360ab1f736fad2639d [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)
giocafd4e62024-07-31 10:53:40 +0400238 if !s.external {
239 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
240 }
gioa60f0de2024-07-08 10:49:48 +0400241 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
242 }()
giocafd4e62024-07-31 10:53:40 +0400243 if !s.external {
244 go func() {
245 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400246 for {
247 delay := time.Duration(rand.Intn(60)+60) * time.Second
248 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400249 s.syncUsers()
250 }
251 }()
252 }
gioa60f0de2024-07-08 10:49:48 +0400253 return <-e
254}
255
gio11617ac2024-07-15 16:09:04 +0400256type UserGetter interface {
257 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400258 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400259}
260
261type externalUserGetter struct {
262 sc *securecookie.SecureCookie
263}
264
265func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400266 return &externalUserGetter{securecookie.New(
267 securecookie.GenerateRandomKey(64),
268 securecookie.GenerateRandomKey(32),
269 )}
gio11617ac2024-07-15 16:09:04 +0400270}
271
272func (ug *externalUserGetter) Get(r *http.Request) string {
273 cookie, err := r.Cookie(sessionCookie)
274 if err != nil {
275 return ""
276 }
277 var user string
278 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
279 return ""
280 }
281 return user
282}
283
gio8fae3af2024-07-25 13:43:31 +0400284func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
285 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
286 cookie := &http.Cookie{
287 Name: sessionCookie,
288 Value: encoded,
289 Path: "/",
290 Secure: true,
291 HttpOnly: true,
292 }
293 http.SetCookie(w, cookie)
294 return nil
295 } else {
296 return err
297 }
298}
299
gio11617ac2024-07-15 16:09:04 +0400300type internalUserGetter struct{}
301
302func NewInternalUserGetter() UserGetter {
303 return internalUserGetter{}
304}
305
306func (ug internalUserGetter) Get(r *http.Request) string {
307 return r.Header.Get("X-User")
308}
309
gio8fae3af2024-07-25 13:43:31 +0400310func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
311 return nil
312}
313
gio81246f02024-07-10 12:02:15 +0400314func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
315 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400316 if strings.HasSuffix(r.URL.Path, loginPath) ||
317 strings.HasPrefix(r.URL.Path, logoutPath) ||
318 strings.HasPrefix(r.URL.Path, staticPath) ||
319 strings.HasPrefix(r.URL.Path, apiPublicData) ||
320 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400321 next.ServeHTTP(w, r)
322 return
323 }
gio11617ac2024-07-15 16:09:04 +0400324 user := s.ug.Get(r)
325 if user == "" {
gio81246f02024-07-10 12:02:15 +0400326 vars := mux.Vars(r)
327 appName, ok := vars["app-name"]
328 if !ok || appName == "" {
329 http.Error(w, "missing app-name", http.StatusBadRequest)
330 return
331 }
332 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
333 return
334 }
gio81246f02024-07-10 12:02:15 +0400335 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
336 })
337}
338
339func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400340 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400341 http.SetCookie(w, &http.Cookie{
342 Name: sessionCookie,
343 Value: "",
344 Path: "/",
345 HttpOnly: true,
346 Secure: true,
347 })
348 http.Redirect(w, r, "/", http.StatusSeeOther)
349}
350
351func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
352 vars := mux.Vars(r)
353 appName, ok := vars["app-name"]
354 if !ok || appName == "" {
355 http.Error(w, "missing app-name", http.StatusBadRequest)
356 return
357 }
358 fmt.Fprint(w, `
359<!DOCTYPE html>
360<html lang='en'>
361 <head>
362 <title>dodo: app - login</title>
363 <meta charset='utf-8'>
364 </head>
365 <body>
366 <form action="" method="POST">
367 <input type="password" placeholder="Password" name="password" required />
368 <button type="submit">Login</button>
369 </form>
370 </body>
371</html>
372`)
373}
374
375func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
376 vars := mux.Vars(r)
377 appName, ok := vars["app-name"]
378 if !ok || appName == "" {
379 http.Error(w, "missing app-name", http.StatusBadRequest)
380 return
381 }
382 password := r.FormValue("password")
383 if password == "" {
384 http.Error(w, "missing password", http.StatusBadRequest)
385 return
386 }
387 user, err := s.st.GetAppOwner(appName)
388 if err != nil {
389 http.Error(w, err.Error(), http.StatusInternalServerError)
390 return
391 }
392 hashed, err := s.st.GetUserPassword(user)
393 if err != nil {
394 http.Error(w, err.Error(), http.StatusInternalServerError)
395 return
396 }
397 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
398 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
399 return
400 }
gio8fae3af2024-07-25 13:43:31 +0400401 if err := s.ug.Encode(w, user); err != nil {
402 http.Error(w, err.Error(), http.StatusInternalServerError)
403 return
gio81246f02024-07-10 12:02:15 +0400404 }
405 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
406}
407
giob4a3a192024-08-19 09:55:47 +0400408type navItem struct {
409 Name string
410 Address string
411}
412
gio23bdc1b2024-07-11 16:07:47 +0400413type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400414 Navigation []navItem
415 Apps []string
416 Networks []installer.Network
417 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400418}
419
gioa60f0de2024-07-08 10:49:48 +0400420func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400421 user := r.Context().Value(userCtx)
422 if user == nil {
423 http.Error(w, "unauthorized", http.StatusUnauthorized)
424 return
425 }
426 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400427 if err != nil {
428 http.Error(w, err.Error(), http.StatusInternalServerError)
429 return
430 }
gio11617ac2024-07-15 16:09:04 +0400431 networks, err := s.getNetworks(user.(string))
432 if err != nil {
433 http.Error(w, err.Error(), http.StatusInternalServerError)
434 return
435 }
giob54db242024-07-30 18:49:33 +0400436 var types []string
437 for _, t := range s.appTmpls.Types() {
438 types = append(types, strings.Replace(t, "-", ":", 1))
439 }
giob4a3a192024-08-19 09:55:47 +0400440 n := []navItem{navItem{"Home", "/"}}
441 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400442 if err := s.tmplts.index.Execute(w, data); err != nil {
443 http.Error(w, err.Error(), http.StatusInternalServerError)
444 return
gioa60f0de2024-07-08 10:49:48 +0400445 }
446}
447
gio5e49bb62024-07-20 10:43:19 +0400448type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400449 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400450 Name string
451 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400452 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400453 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400454 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400455}
456
gioa60f0de2024-07-08 10:49:48 +0400457func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
458 vars := mux.Vars(r)
459 appName, ok := vars["app-name"]
460 if !ok || appName == "" {
461 http.Error(w, "missing app-name", http.StatusBadRequest)
462 return
463 }
gio7fbd4ad2024-08-27 10:06:39 +0400464 branch, ok := vars["branch"]
465 if !ok || branch == "" {
466 branch = "master"
467 }
gio94904702024-07-26 16:58:34 +0400468 u := r.Context().Value(userCtx)
469 if u == nil {
470 http.Error(w, "unauthorized", http.StatusUnauthorized)
471 return
472 }
473 user, ok := u.(string)
474 if !ok {
475 http.Error(w, "could not get user", http.StatusInternalServerError)
476 return
477 }
478 owner, err := s.st.GetAppOwner(appName)
479 if err != nil {
480 http.Error(w, err.Error(), http.StatusInternalServerError)
481 return
482 }
483 if owner != user {
484 http.Error(w, "unauthorized", http.StatusUnauthorized)
485 return
486 }
gio7fbd4ad2024-08-27 10:06:39 +0400487 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400488 if err != nil {
489 http.Error(w, err.Error(), http.StatusInternalServerError)
490 return
491 }
gio183e8342024-08-20 06:01:24 +0400492 var lastCommitResources resourceData
493 if len(commits) > 0 {
494 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
495 if err != nil {
496 http.Error(w, err.Error(), http.StatusInternalServerError)
497 return
498 }
499 r, err := extractResourceData(lastCommit.Resources.Helm)
500 if err != nil {
501 http.Error(w, err.Error(), http.StatusInternalServerError)
502 return
503 }
504 lastCommitResources = r
505 }
gio7fbd4ad2024-08-27 10:06:39 +0400506 branches, err := s.st.GetBranches(appName)
507 if err != nil {
508 http.Error(w, err.Error(), http.StatusInternalServerError)
509 return
510 }
gio5e49bb62024-07-20 10:43:19 +0400511 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400512 Navigation: []navItem{
513 navItem{"Home", "/"},
514 navItem{appName, "/" + appName},
515 },
gio5e49bb62024-07-20 10:43:19 +0400516 Name: appName,
517 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
518 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400519 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400520 Branches: branches,
521 }
522 if branch != "master" {
523 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400524 }
525 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
gioa60f0de2024-07-08 10:49:48 +0400528 }
gio0eaf2712024-04-14 13:08:46 +0400529}
530
giob4a3a192024-08-19 09:55:47 +0400531type volume struct {
532 Name string
533 Size string
534}
535
536type postgresql struct {
537 Name string
538 Version string
539 Volume string
540}
541
542type ingress struct {
543 Host string
544}
545
gio7fbd4ad2024-08-27 10:06:39 +0400546type vm struct {
547 Name string
548 User string
549 CPUCores int
550 Memory string
551}
552
giob4a3a192024-08-19 09:55:47 +0400553type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400554 Volume []volume
555 PostgreSQL []postgresql
556 Ingress []ingress
557 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400558}
559
560type commitStatusData struct {
561 Navigation []navItem
562 AppName string
563 Commit Commit
564 Resources resourceData
565}
566
567func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
568 vars := mux.Vars(r)
569 appName, ok := vars["app-name"]
570 if !ok || appName == "" {
571 http.Error(w, "missing app-name", http.StatusBadRequest)
572 return
573 }
574 hash, ok := vars["hash"]
575 if !ok || appName == "" {
576 http.Error(w, "missing app-name", http.StatusBadRequest)
577 return
578 }
579 u := r.Context().Value(userCtx)
580 if u == nil {
581 http.Error(w, "unauthorized", http.StatusUnauthorized)
582 return
583 }
584 user, ok := u.(string)
585 if !ok {
586 http.Error(w, "could not get user", http.StatusInternalServerError)
587 return
588 }
589 owner, err := s.st.GetAppOwner(appName)
590 if err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
594 if owner != user {
595 http.Error(w, "unauthorized", http.StatusUnauthorized)
596 return
597 }
598 commit, err := s.st.GetCommit(hash)
599 if err != nil {
600 // TODO(gio): not-found ?
601 http.Error(w, err.Error(), http.StatusInternalServerError)
602 return
603 }
604 var res strings.Builder
605 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
606 http.Error(w, err.Error(), http.StatusInternalServerError)
607 return
608 }
609 resData, err := extractResourceData(commit.Resources.Helm)
610 if err != nil {
611 http.Error(w, err.Error(), http.StatusInternalServerError)
612 return
613 }
614 data := commitStatusData{
615 Navigation: []navItem{
616 navItem{"Home", "/"},
617 navItem{appName, "/" + appName},
618 navItem{hash, "/" + appName + "/" + hash},
619 },
620 AppName: appName,
621 Commit: commit,
622 Resources: resData,
623 }
624 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
625 http.Error(w, err.Error(), http.StatusInternalServerError)
626 return
627 }
628}
629
gio183e8342024-08-20 06:01:24 +0400630type logData struct {
631 Navigation []navItem
632 AppName string
633 Logs template.HTML
634}
635
636func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
637 vars := mux.Vars(r)
638 appName, ok := vars["app-name"]
639 if !ok || appName == "" {
640 http.Error(w, "missing app-name", http.StatusBadRequest)
641 return
642 }
643 u := r.Context().Value(userCtx)
644 if u == nil {
645 http.Error(w, "unauthorized", http.StatusUnauthorized)
646 return
647 }
648 user, ok := u.(string)
649 if !ok {
650 http.Error(w, "could not get user", http.StatusInternalServerError)
651 return
652 }
653 owner, err := s.st.GetAppOwner(appName)
654 if err != nil {
655 http.Error(w, err.Error(), http.StatusInternalServerError)
656 return
657 }
658 if owner != user {
659 http.Error(w, "unauthorized", http.StatusUnauthorized)
660 return
661 }
662 data := logData{
663 Navigation: []navItem{
664 navItem{"Home", "/"},
665 navItem{appName, "/" + appName},
666 navItem{"Logs", "/" + appName + "/logs"},
667 },
668 AppName: appName,
669 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
670 }
671 if err := s.tmplts.logs.Execute(w, data); err != nil {
672 fmt.Println(err)
673 http.Error(w, err.Error(), http.StatusInternalServerError)
674 return
675 }
676}
677
gio81246f02024-07-10 12:02:15 +0400678type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400679 Ref string `json:"ref"`
680 Repository struct {
681 Name string `json:"name"`
682 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400683 After string `json:"after"`
684 Commits []struct {
685 Id string `json:"id"`
686 Message string `json:"message"`
687 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400688}
689
gio8fae3af2024-07-25 13:43:31 +0400690func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400691 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400692 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400693 var contents strings.Builder
694 io.Copy(&contents, r.Body)
695 c := contents.String()
696 fmt.Println(c)
697 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400698 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400699 return
700 }
gio7fbd4ad2024-08-27 10:06:39 +0400701 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
702 return
703 }
704 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
705 if !ok {
706 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400707 return
708 }
gioa60f0de2024-07-08 10:49:48 +0400709 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400710 go func() {
gio11617ac2024-07-15 16:09:04 +0400711 owner, err := s.st.GetAppOwner(req.Repository.Name)
712 if err != nil {
713 return
714 }
715 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400716 if err != nil {
717 return
718 }
gio94904702024-07-26 16:58:34 +0400719 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
720 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
721 if err != nil {
722 return
723 }
gioe2e31e12024-08-18 08:20:56 +0400724 found := false
725 commitMsg := ""
726 for _, c := range req.Commits {
727 if c.Id == req.After {
728 found = true
729 commitMsg = c.Message
730 break
gioa60f0de2024-07-08 10:49:48 +0400731 }
732 }
gioe2e31e12024-08-18 08:20:56 +0400733 if !found {
734 fmt.Printf("Error: could not find commit message")
735 return
736 }
gio7fbd4ad2024-08-27 10:06:39 +0400737 s.l.Lock()
738 defer s.l.Unlock()
739 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, owner)
740 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400741 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400742 return
743 }
gioa60f0de2024-07-08 10:49:48 +0400744 for addr, _ := range s.workers[req.Repository.Name] {
745 go func() {
746 // TODO(gio): make port configurable
747 http.Get(fmt.Sprintf("http://%s/update", addr))
748 }()
gio0eaf2712024-04-14 13:08:46 +0400749 }
750 }()
gio0eaf2712024-04-14 13:08:46 +0400751}
752
gio81246f02024-07-10 12:02:15 +0400753type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400754 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400755 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400756}
757
gio8fae3af2024-07-25 13:43:31 +0400758func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400759 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400760 vars := mux.Vars(r)
761 appName, ok := vars["app-name"]
762 if !ok || appName == "" {
763 http.Error(w, "missing app-name", http.StatusBadRequest)
764 return
765 }
gio81246f02024-07-10 12:02:15 +0400766 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400767 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
768 http.Error(w, err.Error(), http.StatusInternalServerError)
769 return
770 }
gioa60f0de2024-07-08 10:49:48 +0400771 if _, ok := s.workers[appName]; !ok {
772 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400773 }
gioa60f0de2024-07-08 10:49:48 +0400774 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400775 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400776}
777
gio11617ac2024-07-15 16:09:04 +0400778func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
779 u := r.Context().Value(userCtx)
780 if u == nil {
781 http.Error(w, "unauthorized", http.StatusUnauthorized)
782 return
783 }
784 user, ok := u.(string)
785 if !ok {
786 http.Error(w, "could not get user", http.StatusInternalServerError)
787 return
788 }
789 network := r.FormValue("network")
790 if network == "" {
791 http.Error(w, "missing network", http.StatusBadRequest)
792 return
793 }
gio5e49bb62024-07-20 10:43:19 +0400794 subdomain := r.FormValue("subdomain")
795 if subdomain == "" {
796 http.Error(w, "missing subdomain", http.StatusBadRequest)
797 return
798 }
799 appType := r.FormValue("type")
800 if appType == "" {
801 http.Error(w, "missing type", http.StatusBadRequest)
802 return
803 }
gio11617ac2024-07-15 16:09:04 +0400804 g := installer.NewFixedLengthRandomNameGenerator(3)
805 appName, err := g.Generate()
806 if err != nil {
807 http.Error(w, err.Error(), http.StatusInternalServerError)
808 return
809 }
810 if ok, err := s.client.UserExists(user); err != nil {
811 http.Error(w, err.Error(), http.StatusInternalServerError)
812 return
813 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400814 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
815 return
gio11617ac2024-07-15 16:09:04 +0400816 }
giocafd4e62024-07-31 10:53:40 +0400817 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400818 http.Error(w, err.Error(), http.StatusInternalServerError)
819 return
820 }
821 if err := s.st.CreateApp(appName, user); err != nil {
822 http.Error(w, err.Error(), http.StatusInternalServerError)
823 return
824 }
giod8ab4f52024-07-26 16:58:34 +0400825 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400826 http.Error(w, err.Error(), http.StatusInternalServerError)
827 return
828 }
829 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
830}
831
gio7fbd4ad2024-08-27 10:06:39 +0400832func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
833 u := r.Context().Value(userCtx)
834 if u == nil {
835 http.Error(w, "unauthorized", http.StatusUnauthorized)
836 return
837 }
838 user, ok := u.(string)
839 if !ok {
840 http.Error(w, "could not get user", http.StatusInternalServerError)
841 return
842 }
843 vars := mux.Vars(r)
844 appName, ok := vars["app-name"]
845 if !ok || appName == "" {
846 http.Error(w, "missing app-name", http.StatusBadRequest)
847 return
848 }
849 branch := r.FormValue("branch")
850 if branch == "" {
851 http.Error(w, "missing network", http.StatusBadRequest)
852 return
853 }
854 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
855 http.Error(w, err.Error(), http.StatusInternalServerError)
856 return
857 }
858 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
859}
860
gio81246f02024-07-10 12:02:15 +0400861type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400862 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400863 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400864 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400865 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400866}
867
gio81246f02024-07-10 12:02:15 +0400868type apiCreateAppResp struct {
869 AppName string `json:"appName"`
870 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400871}
872
gio8fae3af2024-07-25 13:43:31 +0400873func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400874 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400875 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400876 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
877 http.Error(w, err.Error(), http.StatusBadRequest)
878 return
879 }
880 g := installer.NewFixedLengthRandomNameGenerator(3)
881 appName, err := g.Generate()
882 if err != nil {
883 http.Error(w, err.Error(), http.StatusInternalServerError)
884 return
885 }
gio11617ac2024-07-15 16:09:04 +0400886 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400887 if err != nil {
gio33059762024-07-05 13:19:07 +0400888 http.Error(w, err.Error(), http.StatusInternalServerError)
889 return
890 }
gio11617ac2024-07-15 16:09:04 +0400891 if user != "" {
892 http.Error(w, "public key already registered", http.StatusBadRequest)
893 return
894 }
895 user = appName
896 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
897 http.Error(w, err.Error(), http.StatusInternalServerError)
898 return
899 }
900 password := generatePassword()
901 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
902 if err != nil {
903 http.Error(w, err.Error(), http.StatusInternalServerError)
904 return
905 }
giocafd4e62024-07-31 10:53:40 +0400906 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400907 http.Error(w, err.Error(), http.StatusInternalServerError)
908 return
909 }
910 if err := s.st.CreateApp(appName, user); err != nil {
911 http.Error(w, err.Error(), http.StatusInternalServerError)
912 return
913 }
giod8ab4f52024-07-26 16:58:34 +0400914 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400915 http.Error(w, err.Error(), http.StatusInternalServerError)
916 return
917 }
gio81246f02024-07-10 12:02:15 +0400918 resp := apiCreateAppResp{
919 AppName: appName,
920 Password: password,
921 }
gio33059762024-07-05 13:19:07 +0400922 if err := json.NewEncoder(w).Encode(resp); err != nil {
923 http.Error(w, err.Error(), http.StatusInternalServerError)
924 return
925 }
926}
927
giod8ab4f52024-07-26 16:58:34 +0400928func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400929 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400930 return true
931 }
932 for _, cfg := range s.appConfigs {
933 if strings.ToLower(cfg.Network) == network {
934 return false
935 }
936 }
937 return true
938}
939
940func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400941 s.l.Lock()
942 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400943 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400944 network = strings.ToLower(network)
945 if !s.isNetworkUseAllowed(network) {
946 return fmt.Errorf("network already used: %s", network)
947 }
gio33059762024-07-05 13:19:07 +0400948 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400949 return err
gio33059762024-07-05 13:19:07 +0400950 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400951 return nil
gioa60f0de2024-07-08 10:49:48 +0400952 }
gio5e49bb62024-07-20 10:43:19 +0400953 networks, err := s.getNetworks(user)
954 if err != nil {
955 return err
956 }
giod8ab4f52024-07-26 16:58:34 +0400957 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400958 if !ok {
959 return fmt.Errorf("network not found: %s\n", network)
960 }
gio33059762024-07-05 13:19:07 +0400961 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400962 return err
gio33059762024-07-05 13:19:07 +0400963 }
964 appRepo, err := s.client.GetRepo(appName)
965 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400966 return err
gio33059762024-07-05 13:19:07 +0400967 }
gio7fbd4ad2024-08-27 10:06:39 +0400968 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
969 if err != nil {
970 return err
971 }
972 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
973}
974
975func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
976 s.l.Lock()
977 defer s.l.Unlock()
978 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
979 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
980 if err != nil {
981 return err
982 }
983 appCfg, err := soft.ReadFile(appRepo, "app.cue")
984 if err != nil {
985 return err
986 }
987 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
988 if err != nil {
989 return err
990 }
991 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.cue": branchCfg})
992}
993
994func (s *DodoAppServer) createAppForBranch(
995 repo soft.RepoIO,
996 appName string,
997 branch string,
998 user string,
999 network string,
1000 files map[string][]byte,
1001) error {
1002 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1003 for path, contents := range files {
1004 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1005 return "", err
1006 }
1007 }
1008 return "init", nil
1009 }, soft.WithCommitToBranch(branch))
1010 if err != nil {
1011 return err
1012 }
1013 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001014 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001015 return err
gio33059762024-07-05 13:19:07 +04001016 }
1017 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001018 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1019 if err != nil {
1020 return err
1021 }
1022 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001023 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001024 return err
gio33059762024-07-05 13:19:07 +04001025 }
1026 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1027 suffix, err := suffixGen.Generate()
1028 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001029 return err
gio33059762024-07-05 13:19:07 +04001030 }
gio94904702024-07-26 16:58:34 +04001031 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001032 s.setAppConfig(appName, branch, appConfig{namespace, network})
1033 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, user)
giob4a3a192024-08-19 09:55:47 +04001034 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001035 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001036 return err
1037 }
gio7fbd4ad2024-08-27 10:06:39 +04001038 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001039 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001040 return err
gio33059762024-07-05 13:19:07 +04001041 }
giod8ab4f52024-07-26 16:58:34 +04001042 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001043 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001044 return err
gio33059762024-07-05 13:19:07 +04001045 }
1046 hf := installer.NewGitHelmFetcher()
gio36b23b32024-08-25 12:20:54 +04001047 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, "/")
gio33059762024-07-05 13:19:07 +04001048 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001049 return err
gio33059762024-07-05 13:19:07 +04001050 }
gio7fbd4ad2024-08-27 10:06:39 +04001051 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001052 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001053 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001054 if err != nil {
1055 return "", err
1056 }
1057 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001058 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001059 return "", err
1060 }
1061 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001062 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001063 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001064 appPath,
gio9d66f322024-07-06 13:45:10 +04001065 namespace,
1066 map[string]any{
1067 "repoAddr": s.client.GetRepoAddress(appName),
1068 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001069 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001070 "gitRepoPublicKey": s.gitRepoPublicKey,
1071 },
1072 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001073 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001074 installer.WithNoPublish(),
1075 installer.WithNoLock(),
1076 ); err != nil {
1077 return "", err
1078 }
1079 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001080 })
1081 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001082 return err
gio33059762024-07-05 13:19:07 +04001083 }
gio7fbd4ad2024-08-27 10:06:39 +04001084 return s.initAppACLs(m, appPath, appName, branch, user)
1085}
1086
1087func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
1088 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001089 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001090 return err
gio33059762024-07-05 13:19:07 +04001091 }
1092 fluxKeys, ok := cfg.Input["fluxKeys"]
1093 if !ok {
gio11617ac2024-07-15 16:09:04 +04001094 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001095 }
1096 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1097 if !ok {
gio11617ac2024-07-15 16:09:04 +04001098 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001099 }
1100 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001101 return err
gio33059762024-07-05 13:19:07 +04001102 } else if ok {
1103 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001104 return err
gio33059762024-07-05 13:19:07 +04001105 }
1106 } else {
1107 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001108 return err
gio33059762024-07-05 13:19:07 +04001109 }
1110 }
gio7fbd4ad2024-08-27 10:06:39 +04001111 if branch != "master" {
1112 return nil
1113 }
gio33059762024-07-05 13:19:07 +04001114 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001115 return err
gio33059762024-07-05 13:19:07 +04001116 }
gio7fbd4ad2024-08-27 10:06:39 +04001117 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001118 return err
gio33059762024-07-05 13:19:07 +04001119 }
gio7fbd4ad2024-08-27 10:06:39 +04001120 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 +04001121 return err
gio33059762024-07-05 13:19:07 +04001122 }
gio2ccb6e32024-08-15 12:01:33 +04001123 if !s.external {
1124 go func() {
1125 users, err := s.client.GetAllUsers()
1126 if err != nil {
1127 fmt.Println(err)
1128 return
1129 }
1130 for _, user := range users {
1131 // TODO(gio): fluxcd should have only read access
1132 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1133 fmt.Println(err)
1134 }
1135 }
1136 }()
1137 }
gio43b0f422024-08-21 10:40:13 +04001138 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1139 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001140 return nil
gio33059762024-07-05 13:19:07 +04001141}
1142
gio81246f02024-07-10 12:02:15 +04001143type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001144 User string `json:"user"`
1145 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001146}
1147
gio7fbd4ad2024-08-27 10:06:39 +04001148func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001149 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001150 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1151 http.Error(w, err.Error(), http.StatusBadRequest)
1152 return
1153 }
gio7fbd4ad2024-08-27 10:06:39 +04001154 if req.User == "" {
1155 http.Error(w, "invalid user", http.StatusBadRequest)
1156 return
1157 }
1158 if req.PublicKey == "" {
1159 http.Error(w, "invalid public key", http.StatusBadRequest)
1160 return
1161 }
1162 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001163 http.Error(w, err.Error(), http.StatusInternalServerError)
1164 return
1165 }
1166}
1167
gio94904702024-07-26 16:58:34 +04001168type dodoAppRendered struct {
1169 App struct {
1170 Ingress struct {
1171 Network string `json:"network"`
1172 Subdomain string `json:"subdomain"`
1173 } `json:"ingress"`
1174 } `json:"app"`
1175 Input struct {
1176 AppId string `json:"appId"`
1177 } `json:"input"`
1178}
1179
gio7fbd4ad2024-08-27 10:06:39 +04001180// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio43b0f422024-08-21 10:40:13 +04001181func (s *DodoAppServer) updateDodoApp(
1182 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001183 name string,
1184 branch string,
1185 namespace string,
gio43b0f422024-08-21 10:40:13 +04001186 networks []installer.Network,
gio7fbd4ad2024-08-27 10:06:39 +04001187 owner string,
gio43b0f422024-08-21 10:40:13 +04001188) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001189 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001190 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001191 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001192 }
giof8843412024-05-22 16:38:05 +04001193 hf := installer.NewGitHelmFetcher()
gio36b23b32024-08-25 12:20:54 +04001194 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001195 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001196 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001197 }
1198 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +04001199 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001200 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001201 }
1202 app, err := installer.NewDodoApp(appCfg)
1203 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001204 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001205 }
giof8843412024-05-22 16:38:05 +04001206 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001207 var ret installer.ReleaseResources
1208 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1209 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001210 app,
1211 "app",
1212 "/.dodo/app",
1213 namespace,
1214 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001215 "repoAddr": repo.FullAddress(),
1216 "repoPublicAddr": s.repoPublicAddr,
1217 "managerAddr": fmt.Sprintf("http://%s", s.self),
1218 "appId": name,
1219 "branch": branch,
1220 "sshPrivateKey": s.sshKey,
1221 "username": owner,
gio94904702024-07-26 16:58:34 +04001222 },
1223 installer.WithNoPull(),
1224 installer.WithNoPublish(),
1225 installer.WithConfig(&s.env),
1226 installer.WithNetworks(networks),
1227 installer.WithLocalChartGenerator(lg),
1228 installer.WithNoLock(),
1229 )
1230 if err != nil {
1231 return "", err
1232 }
1233 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001234 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001235 return "", nil
1236 }
1237 if _, err := m.Install(
1238 appStatus,
1239 "status",
1240 "/.dodo/status",
1241 s.namespace,
1242 map[string]any{
1243 "appName": rendered.Input.AppId,
1244 "network": rendered.App.Ingress.Network,
1245 "appSubdomain": rendered.App.Ingress.Subdomain,
1246 },
1247 installer.WithNoPull(),
1248 installer.WithNoPublish(),
1249 installer.WithConfig(&s.env),
1250 installer.WithNetworks(networks),
1251 installer.WithLocalChartGenerator(lg),
1252 installer.WithNoLock(),
1253 ); err != nil {
1254 return "", err
1255 }
1256 return "install app", nil
1257 },
gio7fbd4ad2024-08-27 10:06:39 +04001258 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001259 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001260 ); err != nil {
1261 return installer.ReleaseResources{}, err
1262 }
gio43b0f422024-08-21 10:40:13 +04001263 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1264 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001265 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001266}
gio33059762024-07-05 13:19:07 +04001267
gio7fbd4ad2024-08-27 10:06:39 +04001268func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001269 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001270 appTmpl, err := s.appTmpls.Find(appType)
1271 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001272 return nil, err
gio33059762024-07-05 13:19:07 +04001273 }
gio7fbd4ad2024-08-27 10:06:39 +04001274 return appTmpl.Render(network, subdomain)
gio33059762024-07-05 13:19:07 +04001275}
gio81246f02024-07-10 12:02:15 +04001276
1277func generatePassword() string {
1278 return "foo"
1279}
giocb34ad22024-07-11 08:01:13 +04001280
gio11617ac2024-07-15 16:09:04 +04001281func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001282 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001283 resp, err := http.Get(addr)
1284 if err != nil {
1285 return nil, err
1286 }
gio23bdc1b2024-07-11 16:07:47 +04001287 networks := []installer.Network{}
1288 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001289 return nil, err
1290 }
gio11617ac2024-07-15 16:09:04 +04001291 return s.nf.Filter(user, networks)
1292}
1293
gio8fae3af2024-07-25 13:43:31 +04001294type publicNetworkData struct {
1295 Name string `json:"name"`
1296 Domain string `json:"domain"`
1297}
1298
1299type publicData struct {
1300 Networks []publicNetworkData `json:"networks"`
1301 Types []string `json:"types"`
1302}
1303
1304func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001305 w.Header().Set("Access-Control-Allow-Origin", "*")
1306 s.l.Lock()
1307 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001308 networks, err := s.getNetworks("")
1309 if err != nil {
1310 http.Error(w, err.Error(), http.StatusInternalServerError)
1311 return
1312 }
1313 var ret publicData
1314 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001315 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1316 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1317 }
gio8fae3af2024-07-25 13:43:31 +04001318 }
1319 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001320 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001321 }
gio8fae3af2024-07-25 13:43:31 +04001322 if err := json.NewEncoder(w).Encode(ret); err != nil {
1323 http.Error(w, err.Error(), http.StatusInternalServerError)
1324 return
1325 }
1326}
1327
gio7fbd4ad2024-08-27 10:06:39 +04001328func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001329 if err != nil {
1330 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001331 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001332 fmt.Printf("Error: %s\n", err.Error())
1333 return err
1334 }
1335 return err
1336 }
1337 var resB bytes.Buffer
1338 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001339 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001340 fmt.Printf("Error: %s\n", err.Error())
1341 return err
1342 }
1343 return err
1344 }
gio7fbd4ad2024-08-27 10:06:39 +04001345 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001346 fmt.Printf("Error: %s\n", err.Error())
1347 return err
1348 }
1349 return nil
1350}
1351
gio11617ac2024-07-15 16:09:04 +04001352func pickNetwork(networks []installer.Network, network string) []installer.Network {
1353 for _, n := range networks {
1354 if n.Name == network {
1355 return []installer.Network{n}
1356 }
1357 }
1358 return []installer.Network{}
1359}
1360
1361type NetworkFilter interface {
1362 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1363}
1364
1365type noNetworkFilter struct{}
1366
1367func NewNoNetworkFilter() NetworkFilter {
1368 return noNetworkFilter{}
1369}
1370
gio8fae3af2024-07-25 13:43:31 +04001371func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001372 return networks, nil
1373}
1374
1375type filterByOwner struct {
1376 st Store
1377}
1378
1379func NewNetworkFilterByOwner(st Store) NetworkFilter {
1380 return &filterByOwner{st}
1381}
1382
1383func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001384 if user == "" {
1385 return networks, nil
1386 }
gio11617ac2024-07-15 16:09:04 +04001387 network, err := f.st.GetUserNetwork(user)
1388 if err != nil {
1389 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001390 }
1391 ret := []installer.Network{}
1392 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001393 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001394 ret = append(ret, n)
1395 }
1396 }
giocb34ad22024-07-11 08:01:13 +04001397 return ret, nil
1398}
gio11617ac2024-07-15 16:09:04 +04001399
1400type allowListFilter struct {
1401 allowed []string
1402}
1403
1404func NewAllowListFilter(allowed []string) NetworkFilter {
1405 return &allowListFilter{allowed}
1406}
1407
gio8fae3af2024-07-25 13:43:31 +04001408func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001409 ret := []installer.Network{}
1410 for _, n := range networks {
1411 if slices.Contains(f.allowed, n.Name) {
1412 ret = append(ret, n)
1413 }
1414 }
1415 return ret, nil
1416}
1417
1418type combinedNetworkFilter struct {
1419 filters []NetworkFilter
1420}
1421
1422func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1423 return &combinedNetworkFilter{filters}
1424}
1425
gio8fae3af2024-07-25 13:43:31 +04001426func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001427 ret := networks
1428 var err error
1429 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001430 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001431 if err != nil {
1432 return nil, err
1433 }
1434 }
1435 return ret, nil
1436}
giocafd4e62024-07-31 10:53:40 +04001437
1438type user struct {
1439 Username string `json:"username"`
1440 Email string `json:"email"`
1441 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1442}
1443
1444func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1445 go s.syncUsers()
1446}
1447
1448func (s *DodoAppServer) syncUsers() {
1449 if s.external {
1450 panic("MUST NOT REACH!")
1451 }
1452 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1453 if err != nil {
1454 return
1455 }
1456 users := []user{}
1457 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1458 fmt.Println(err)
1459 return
1460 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001461 validUsernames := make(map[string]user)
1462 for _, u := range users {
1463 validUsernames[u.Username] = u
1464 }
1465 allClientUsers, err := s.client.GetAllUsers()
1466 if err != nil {
1467 fmt.Println(err)
1468 return
1469 }
1470 keyToUser := make(map[string]string)
1471 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001472 if clientUser == "admin" || clientUser == "fluxcd" {
1473 continue
1474 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001475 userData, ok := validUsernames[clientUser]
1476 if !ok {
1477 if err := s.client.RemoveUser(clientUser); err != nil {
1478 fmt.Println(err)
1479 return
1480 }
1481 } else {
1482 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1483 if err != nil {
1484 fmt.Println(err)
1485 return
1486 }
1487 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001488 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001489 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001490 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001491 })
1492 if !keyOk {
1493 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1494 fmt.Println(err)
1495 }
1496 } else {
1497 keyToUser[cleanKey] = clientUser
1498 }
1499 }
1500 }
1501 }
giocafd4e62024-07-31 10:53:40 +04001502 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001503 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1504 fmt.Println(err)
1505 return
1506 }
giocafd4e62024-07-31 10:53:40 +04001507 if len(u.SSHPublicKeys) == 0 {
1508 continue
1509 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001510 ok, err := s.client.UserExists(u.Username)
1511 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001512 fmt.Println(err)
1513 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001514 }
1515 if !ok {
1516 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1517 fmt.Println(err)
1518 return
1519 }
1520 } else {
1521 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001522 cleanKey := soft.CleanKey(key)
1523 if user, ok := keyToUser[cleanKey]; ok {
1524 if u.Username != user {
1525 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1526 }
1527 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001528 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001529 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001530 fmt.Println(err)
1531 return
giocafd4e62024-07-31 10:53:40 +04001532 }
1533 }
1534 }
1535 }
1536 repos, err := s.client.GetAllRepos()
1537 if err != nil {
1538 return
1539 }
1540 for _, r := range repos {
1541 if r == ConfigRepoName {
1542 continue
1543 }
1544 for _, u := range users {
1545 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1546 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001547 continue
giocafd4e62024-07-31 10:53:40 +04001548 }
1549 }
1550 }
1551}
giob4a3a192024-08-19 09:55:47 +04001552
1553func extractResourceData(resources []installer.Resource) (resourceData, error) {
1554 var ret resourceData
1555 for _, r := range resources {
1556 t, ok := r.Annotations["dodo.cloud/resource-type"]
1557 if !ok {
1558 continue
1559 }
1560 switch t {
1561 case "volume":
1562 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1563 if !ok {
1564 return resourceData{}, fmt.Errorf("no name")
1565 }
1566 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1567 if !ok {
1568 return resourceData{}, fmt.Errorf("no size")
1569 }
1570 ret.Volume = append(ret.Volume, volume{name, size})
1571 case "postgresql":
1572 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1573 if !ok {
1574 return resourceData{}, fmt.Errorf("no name")
1575 }
1576 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1577 if !ok {
1578 return resourceData{}, fmt.Errorf("no version")
1579 }
1580 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1581 if !ok {
1582 return resourceData{}, fmt.Errorf("no volume")
1583 }
1584 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1585 case "ingress":
1586 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1587 if !ok {
1588 return resourceData{}, fmt.Errorf("no host")
1589 }
1590 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001591 case "virtual-machine":
1592 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1593 if !ok {
1594 return resourceData{}, fmt.Errorf("no name")
1595 }
1596 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1597 if !ok {
1598 return resourceData{}, fmt.Errorf("no user")
1599 }
1600 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1601 if !ok {
1602 return resourceData{}, fmt.Errorf("no cpu cores")
1603 }
1604 cpuCores, err := strconv.Atoi(cpuCoresS)
1605 if err != nil {
1606 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1607 }
1608 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1609 if !ok {
1610 return resourceData{}, fmt.Errorf("no memory")
1611 }
1612 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001613 default:
1614 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1615 }
1616 }
1617 return ret, nil
1618}
gio7fbd4ad2024-08-27 10:06:39 +04001619
1620func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
1621 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{"app.cue": from})
1622 if err != nil {
1623 return "", nil, err
1624 }
1625 if err := cfg.Err(); err != nil {
1626 return "", nil, err
1627 }
1628 if err := cfg.Validate(); err != nil {
1629 return "", nil, err
1630 }
1631 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1632 if err := subdomain.Err(); err != nil {
1633 return "", nil, err
1634 }
1635 subdomainStr, err := subdomain.String()
1636 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1637 if err := network.Err(); err != nil {
1638 return "", nil, err
1639 }
1640 networkStr, err := network.String()
1641 if err != nil {
1642 return "", nil, err
1643 }
1644 newCfg := map[string]any{}
1645 if err := cfg.Decode(&newCfg); err != nil {
1646 return "", nil, err
1647 }
1648 app, ok := newCfg["app"].(map[string]any)
1649 if !ok {
1650 return "", nil, fmt.Errorf("not a map")
1651 }
1652 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1653 app["dev"] = map[string]any{
1654 "enabled": true,
1655 "username": username,
1656 }
1657 buf, err := json.MarshalIndent(newCfg, "", "\t")
1658 if err != nil {
1659 return "", nil, err
1660 }
1661 return networkStr, buf, nil
1662}