blob: d71bf835ebdaaaa87fe595e5f040bf4045230354 [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
gioc81a8472024-09-24 13:06:19 +020038//go:embed stat/schemas/app.schema.json
39var dodoAppJsonSchema []byte
40
gio9d66f322024-07-06 13:45:10 +040041const (
gioa60f0de2024-07-08 10:49:48 +040042 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040043 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040044 loginPath = "/login"
45 logoutPath = "/logout"
gio1bf00802024-08-17 12:31:41 +040046 staticPath = "/stat/"
gioc81a8472024-09-24 13:06:19 +020047 schemasPath = "/schemas/"
gio8fae3af2024-07-25 13:43:31 +040048 apiPublicData = "/api/public-data"
49 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040050 sessionCookie = "dodo-app-session"
51 userCtx = "user"
giob4a3a192024-08-19 09:55:47 +040052 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040053)
54
gio23bdc1b2024-07-11 16:07:47 +040055type dodoAppTmplts struct {
giob4a3a192024-08-19 09:55:47 +040056 index *template.Template
57 appStatus *template.Template
58 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040059 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040060}
61
62func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040063 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040064 if err != nil {
65 return dodoAppTmplts{}, err
66 }
gio5e49bb62024-07-20 10:43:19 +040067 parse := func(path string) (*template.Template, error) {
68 if b, err := base.Clone(); err != nil {
69 return nil, err
70 } else {
71 return b.ParseFS(fs, path)
72 }
73 }
74 index, err := parse("dodo-app-tmpl/index.html")
75 if err != nil {
76 return dodoAppTmplts{}, err
77 }
78 appStatus, err := parse("dodo-app-tmpl/app_status.html")
79 if err != nil {
80 return dodoAppTmplts{}, err
81 }
giob4a3a192024-08-19 09:55:47 +040082 commitStatus, err := parse("dodo-app-tmpl/commit_status.html")
83 if err != nil {
84 return dodoAppTmplts{}, err
85 }
gio183e8342024-08-20 06:01:24 +040086 logs, err := parse("dodo-app-tmpl/logs.html")
87 if err != nil {
88 return dodoAppTmplts{}, err
89 }
90 return dodoAppTmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040091}
92
gio0eaf2712024-04-14 13:08:46 +040093type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040094 l sync.Locker
95 st Store
gio11617ac2024-07-15 16:09:04 +040096 nf NetworkFilter
97 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040098 port int
99 apiPort int
100 self string
gioc81a8472024-09-24 13:06:19 +0200101 selfPublic string
gio11617ac2024-07-15 16:09:04 +0400102 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +0400103 sshKey string
104 gitRepoPublicKey string
105 client soft.Client
106 namespace string
107 envAppManagerAddr string
108 env installer.EnvConfig
109 nsc installer.NamespaceCreator
110 jc installer.JobCreator
gio864b4332024-09-05 13:56:47 +0400111 vpnKeyGen installer.VPNAPIClient
giof6ad2982024-08-23 17:42:49 +0400112 cnc installer.ClusterNetworkConfigurator
giocb34ad22024-07-11 08:01:13 +0400113 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400114 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +0400115 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +0400116 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400117 external bool
118 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400119 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400120 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400121}
122
123type appConfig struct {
124 Namespace string `json:"namespace"`
125 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400126}
127
gio33059762024-07-05 13:19:07 +0400128// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400129func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400130 st Store,
gio11617ac2024-07-15 16:09:04 +0400131 nf NetworkFilter,
132 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400133 port int,
gioa60f0de2024-07-08 10:49:48 +0400134 apiPort int,
gio33059762024-07-05 13:19:07 +0400135 self string,
gioc81a8472024-09-24 13:06:19 +0200136 selfPublic string,
gio11617ac2024-07-15 16:09:04 +0400137 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400138 sshKey string,
gio33059762024-07-05 13:19:07 +0400139 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400140 client soft.Client,
141 namespace string,
giocb34ad22024-07-11 08:01:13 +0400142 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400143 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400144 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400145 vpnKeyGen installer.VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +0400146 cnc installer.ClusterNetworkConfigurator,
gio0eaf2712024-04-14 13:08:46 +0400147 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400148 external bool,
149 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400150 reconciler tasks.Reconciler,
gio9d66f322024-07-06 13:45:10 +0400151) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400152 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
153 if err != nil {
154 return nil, err
155 }
gio5e49bb62024-07-20 10:43:19 +0400156 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
157 if err != nil {
158 return nil, err
159 }
160 appTmpls, err := NewAppTmplStoreFS(apps)
161 if err != nil {
162 return nil, err
163 }
gio9d66f322024-07-06 13:45:10 +0400164 s := &DodoAppServer{
165 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400166 st,
gio11617ac2024-07-15 16:09:04 +0400167 nf,
168 ug,
gio0eaf2712024-04-14 13:08:46 +0400169 port,
gioa60f0de2024-07-08 10:49:48 +0400170 apiPort,
gio33059762024-07-05 13:19:07 +0400171 self,
gioc81a8472024-09-24 13:06:19 +0200172 selfPublic,
gio11617ac2024-07-15 16:09:04 +0400173 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400174 sshKey,
gio33059762024-07-05 13:19:07 +0400175 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400176 client,
177 namespace,
giocb34ad22024-07-11 08:01:13 +0400178 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400179 env,
gio33059762024-07-05 13:19:07 +0400180 nsc,
giof8843412024-05-22 16:38:05 +0400181 jc,
gio36b23b32024-08-25 12:20:54 +0400182 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +0400183 cnc,
gio266c04f2024-07-03 14:18:45 +0400184 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400185 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400186 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400187 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400188 external,
189 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400190 reconciler,
gio183e8342024-08-20 06:01:24 +0400191 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400192 }
gioa60f0de2024-07-08 10:49:48 +0400193 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400194 if err != nil {
195 return nil, err
196 }
giod8ab4f52024-07-26 16:58:34 +0400197 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400198 if err == nil {
199 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400200 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400201 return nil, err
202 }
203 } else if !errors.Is(err, fs.ErrNotExist) {
204 return nil, err
205 }
206 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400207}
208
gio7fbd4ad2024-08-27 10:06:39 +0400209func (s *DodoAppServer) getAppConfig(app, branch string) appConfig {
210 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
211}
212
213func (s *DodoAppServer) setAppConfig(app, branch string, cfg appConfig) {
214 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
215}
216
gio0eaf2712024-04-14 13:08:46 +0400217func (s *DodoAppServer) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400218 // if err := s.client.DisableKeyless(); err != nil {
219 // return err
220 // }
221 // if err := s.client.DisableAnonAccess(); err != nil {
222 // return err
223 // }
gioa60f0de2024-07-08 10:49:48 +0400224 e := make(chan error)
225 go func() {
226 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400227 r.Use(s.mwAuth)
gioc81a8472024-09-24 13:06:19 +0200228 r.HandleFunc(schemasPath+"app.schema.json", s.handleSchema).Methods(http.MethodGet)
gio1bf00802024-08-17 12:31:41 +0400229 r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
gio81246f02024-07-10 12:02:15 +0400230 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400231 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
232 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400233 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
234 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400235 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400236 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400237 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
238 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio81246f02024-07-10 12:02:15 +0400239 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
240 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400241 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400242 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
243 }()
244 go func() {
245 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400246 r.HandleFunc("/update", s.handleAPIUpdate)
247 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400248 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400249 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400250 if !s.external {
251 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
252 }
gioa60f0de2024-07-08 10:49:48 +0400253 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
254 }()
giocafd4e62024-07-31 10:53:40 +0400255 if !s.external {
256 go func() {
257 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400258 for {
259 delay := time.Duration(rand.Intn(60)+60) * time.Second
260 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400261 s.syncUsers()
262 }
263 }()
264 }
gioa60f0de2024-07-08 10:49:48 +0400265 return <-e
266}
267
gio11617ac2024-07-15 16:09:04 +0400268type UserGetter interface {
269 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400270 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400271}
272
273type externalUserGetter struct {
274 sc *securecookie.SecureCookie
275}
276
277func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400278 return &externalUserGetter{securecookie.New(
279 securecookie.GenerateRandomKey(64),
280 securecookie.GenerateRandomKey(32),
281 )}
gio11617ac2024-07-15 16:09:04 +0400282}
283
284func (ug *externalUserGetter) Get(r *http.Request) string {
285 cookie, err := r.Cookie(sessionCookie)
286 if err != nil {
287 return ""
288 }
289 var user string
290 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
291 return ""
292 }
293 return user
294}
295
gio8fae3af2024-07-25 13:43:31 +0400296func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
297 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
298 cookie := &http.Cookie{
299 Name: sessionCookie,
300 Value: encoded,
301 Path: "/",
302 Secure: true,
303 HttpOnly: true,
304 }
305 http.SetCookie(w, cookie)
306 return nil
307 } else {
308 return err
309 }
310}
311
gio11617ac2024-07-15 16:09:04 +0400312type internalUserGetter struct{}
313
314func NewInternalUserGetter() UserGetter {
315 return internalUserGetter{}
316}
317
318func (ug internalUserGetter) Get(r *http.Request) string {
319 return r.Header.Get("X-User")
320}
321
gio8fae3af2024-07-25 13:43:31 +0400322func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
323 return nil
324}
325
gio81246f02024-07-10 12:02:15 +0400326func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
327 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400328 if strings.HasSuffix(r.URL.Path, loginPath) ||
329 strings.HasPrefix(r.URL.Path, logoutPath) ||
330 strings.HasPrefix(r.URL.Path, staticPath) ||
gioc81a8472024-09-24 13:06:19 +0200331 strings.HasPrefix(r.URL.Path, schemasPath) ||
gio8fae3af2024-07-25 13:43:31 +0400332 strings.HasPrefix(r.URL.Path, apiPublicData) ||
333 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400334 next.ServeHTTP(w, r)
335 return
336 }
gio11617ac2024-07-15 16:09:04 +0400337 user := s.ug.Get(r)
338 if user == "" {
gio81246f02024-07-10 12:02:15 +0400339 vars := mux.Vars(r)
340 appName, ok := vars["app-name"]
341 if !ok || appName == "" {
342 http.Error(w, "missing app-name", http.StatusBadRequest)
343 return
344 }
345 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
346 return
347 }
gio81246f02024-07-10 12:02:15 +0400348 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
349 })
350}
351
gioc81a8472024-09-24 13:06:19 +0200352func (s *DodoAppServer) handleSchema(w http.ResponseWriter, r *http.Request) {
353 w.Header().Set("Content-Type", "application/schema+json")
354 w.Write(dodoAppJsonSchema)
355}
356
gio81246f02024-07-10 12:02:15 +0400357func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400358 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400359 http.SetCookie(w, &http.Cookie{
360 Name: sessionCookie,
361 Value: "",
362 Path: "/",
363 HttpOnly: true,
364 Secure: true,
365 })
366 http.Redirect(w, r, "/", http.StatusSeeOther)
367}
368
369func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
370 vars := mux.Vars(r)
371 appName, ok := vars["app-name"]
372 if !ok || appName == "" {
373 http.Error(w, "missing app-name", http.StatusBadRequest)
374 return
375 }
376 fmt.Fprint(w, `
377<!DOCTYPE html>
378<html lang='en'>
379 <head>
380 <title>dodo: app - login</title>
381 <meta charset='utf-8'>
382 </head>
383 <body>
384 <form action="" method="POST">
385 <input type="password" placeholder="Password" name="password" required />
386 <button type="submit">Login</button>
387 </form>
388 </body>
389</html>
390`)
391}
392
393func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
394 vars := mux.Vars(r)
395 appName, ok := vars["app-name"]
396 if !ok || appName == "" {
397 http.Error(w, "missing app-name", http.StatusBadRequest)
398 return
399 }
400 password := r.FormValue("password")
401 if password == "" {
402 http.Error(w, "missing password", http.StatusBadRequest)
403 return
404 }
405 user, err := s.st.GetAppOwner(appName)
406 if err != nil {
407 http.Error(w, err.Error(), http.StatusInternalServerError)
408 return
409 }
410 hashed, err := s.st.GetUserPassword(user)
411 if err != nil {
412 http.Error(w, err.Error(), http.StatusInternalServerError)
413 return
414 }
415 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
416 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
417 return
418 }
gio8fae3af2024-07-25 13:43:31 +0400419 if err := s.ug.Encode(w, user); err != nil {
420 http.Error(w, err.Error(), http.StatusInternalServerError)
421 return
gio81246f02024-07-10 12:02:15 +0400422 }
423 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
424}
425
giob4a3a192024-08-19 09:55:47 +0400426type navItem struct {
427 Name string
428 Address string
429}
430
gio23bdc1b2024-07-11 16:07:47 +0400431type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400432 Navigation []navItem
433 Apps []string
434 Networks []installer.Network
435 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400436}
437
gioa60f0de2024-07-08 10:49:48 +0400438func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400439 user := r.Context().Value(userCtx)
440 if user == nil {
441 http.Error(w, "unauthorized", http.StatusUnauthorized)
442 return
443 }
444 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400445 if err != nil {
446 http.Error(w, err.Error(), http.StatusInternalServerError)
447 return
448 }
gio11617ac2024-07-15 16:09:04 +0400449 networks, err := s.getNetworks(user.(string))
450 if err != nil {
451 http.Error(w, err.Error(), http.StatusInternalServerError)
452 return
453 }
giob54db242024-07-30 18:49:33 +0400454 var types []string
455 for _, t := range s.appTmpls.Types() {
456 types = append(types, strings.Replace(t, "-", ":", 1))
457 }
giob4a3a192024-08-19 09:55:47 +0400458 n := []navItem{navItem{"Home", "/"}}
459 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400460 if err := s.tmplts.index.Execute(w, data); err != nil {
461 http.Error(w, err.Error(), http.StatusInternalServerError)
462 return
gioa60f0de2024-07-08 10:49:48 +0400463 }
464}
465
gio5e49bb62024-07-20 10:43:19 +0400466type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400467 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400468 Name string
469 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400470 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400471 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400472 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400473}
474
gioa60f0de2024-07-08 10:49:48 +0400475func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
476 vars := mux.Vars(r)
477 appName, ok := vars["app-name"]
478 if !ok || appName == "" {
479 http.Error(w, "missing app-name", http.StatusBadRequest)
480 return
481 }
gio7fbd4ad2024-08-27 10:06:39 +0400482 branch, ok := vars["branch"]
483 if !ok || branch == "" {
484 branch = "master"
485 }
gio94904702024-07-26 16:58:34 +0400486 u := r.Context().Value(userCtx)
487 if u == nil {
488 http.Error(w, "unauthorized", http.StatusUnauthorized)
489 return
490 }
491 user, ok := u.(string)
492 if !ok {
493 http.Error(w, "could not get user", http.StatusInternalServerError)
494 return
495 }
496 owner, err := s.st.GetAppOwner(appName)
497 if err != nil {
498 http.Error(w, err.Error(), http.StatusInternalServerError)
499 return
500 }
501 if owner != user {
502 http.Error(w, "unauthorized", http.StatusUnauthorized)
503 return
504 }
gio7fbd4ad2024-08-27 10:06:39 +0400505 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400506 if err != nil {
507 http.Error(w, err.Error(), http.StatusInternalServerError)
508 return
509 }
gio183e8342024-08-20 06:01:24 +0400510 var lastCommitResources resourceData
511 if len(commits) > 0 {
512 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
513 if err != nil {
514 http.Error(w, err.Error(), http.StatusInternalServerError)
515 return
516 }
517 r, err := extractResourceData(lastCommit.Resources.Helm)
518 if err != nil {
519 http.Error(w, err.Error(), http.StatusInternalServerError)
520 return
521 }
522 lastCommitResources = r
523 }
gio7fbd4ad2024-08-27 10:06:39 +0400524 branches, err := s.st.GetBranches(appName)
525 if err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
528 }
gio5e49bb62024-07-20 10:43:19 +0400529 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400530 Navigation: []navItem{
531 navItem{"Home", "/"},
532 navItem{appName, "/" + appName},
533 },
gio5e49bb62024-07-20 10:43:19 +0400534 Name: appName,
535 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
536 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400537 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400538 Branches: branches,
539 }
540 if branch != "master" {
541 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400542 }
543 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
544 http.Error(w, err.Error(), http.StatusInternalServerError)
545 return
gioa60f0de2024-07-08 10:49:48 +0400546 }
gio0eaf2712024-04-14 13:08:46 +0400547}
548
giocfb228c2024-09-06 15:44:31 +0400549type appEnv struct {
550 Profile string `json:"envProfile"`
551}
552
553func (s *DodoAppServer) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
554 vars := mux.Vars(r)
555 appName, ok := vars["app-name"]
556 if !ok || appName == "" {
557 http.Error(w, "missing app-name", http.StatusBadRequest)
558 return
559 }
560 branch, ok := vars["branch"]
561 if !ok || branch == "" {
562 branch = "master"
563 }
564 info, err := s.st.GetLastCommitInfo(appName, branch)
565 if err != nil {
566 http.Error(w, err.Error(), http.StatusInternalServerError)
567 return
568 }
569 var e appEnv
570 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
571 http.Error(w, err.Error(), http.StatusInternalServerError)
572 return
573 }
574 fmt.Fprintln(w, e.Profile)
575}
576
giob4a3a192024-08-19 09:55:47 +0400577type volume struct {
578 Name string
579 Size string
580}
581
582type postgresql struct {
583 Name string
584 Version string
585 Volume string
586}
587
588type ingress struct {
589 Host string
590}
591
gio7fbd4ad2024-08-27 10:06:39 +0400592type vm struct {
593 Name string
594 User string
595 CPUCores int
596 Memory string
597}
598
giob4a3a192024-08-19 09:55:47 +0400599type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400600 Volume []volume
601 PostgreSQL []postgresql
602 Ingress []ingress
603 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400604}
605
606type commitStatusData struct {
607 Navigation []navItem
608 AppName string
609 Commit Commit
610 Resources resourceData
611}
612
613func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
614 vars := mux.Vars(r)
615 appName, ok := vars["app-name"]
616 if !ok || appName == "" {
617 http.Error(w, "missing app-name", http.StatusBadRequest)
618 return
619 }
620 hash, ok := vars["hash"]
621 if !ok || appName == "" {
622 http.Error(w, "missing app-name", http.StatusBadRequest)
623 return
624 }
625 u := r.Context().Value(userCtx)
626 if u == nil {
627 http.Error(w, "unauthorized", http.StatusUnauthorized)
628 return
629 }
630 user, ok := u.(string)
631 if !ok {
632 http.Error(w, "could not get user", http.StatusInternalServerError)
633 return
634 }
635 owner, err := s.st.GetAppOwner(appName)
636 if err != nil {
637 http.Error(w, err.Error(), http.StatusInternalServerError)
638 return
639 }
640 if owner != user {
641 http.Error(w, "unauthorized", http.StatusUnauthorized)
642 return
643 }
644 commit, err := s.st.GetCommit(hash)
645 if err != nil {
646 // TODO(gio): not-found ?
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
650 var res strings.Builder
651 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
652 http.Error(w, err.Error(), http.StatusInternalServerError)
653 return
654 }
655 resData, err := extractResourceData(commit.Resources.Helm)
656 if err != nil {
657 http.Error(w, err.Error(), http.StatusInternalServerError)
658 return
659 }
660 data := commitStatusData{
661 Navigation: []navItem{
662 navItem{"Home", "/"},
663 navItem{appName, "/" + appName},
664 navItem{hash, "/" + appName + "/" + hash},
665 },
666 AppName: appName,
667 Commit: commit,
668 Resources: resData,
669 }
670 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
671 http.Error(w, err.Error(), http.StatusInternalServerError)
672 return
673 }
674}
675
gio183e8342024-08-20 06:01:24 +0400676type logData struct {
677 Navigation []navItem
678 AppName string
679 Logs template.HTML
680}
681
682func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
683 vars := mux.Vars(r)
684 appName, ok := vars["app-name"]
685 if !ok || appName == "" {
686 http.Error(w, "missing app-name", http.StatusBadRequest)
687 return
688 }
689 u := r.Context().Value(userCtx)
690 if u == nil {
691 http.Error(w, "unauthorized", http.StatusUnauthorized)
692 return
693 }
694 user, ok := u.(string)
695 if !ok {
696 http.Error(w, "could not get user", http.StatusInternalServerError)
697 return
698 }
699 owner, err := s.st.GetAppOwner(appName)
700 if err != nil {
701 http.Error(w, err.Error(), http.StatusInternalServerError)
702 return
703 }
704 if owner != user {
705 http.Error(w, "unauthorized", http.StatusUnauthorized)
706 return
707 }
708 data := logData{
709 Navigation: []navItem{
710 navItem{"Home", "/"},
711 navItem{appName, "/" + appName},
712 navItem{"Logs", "/" + appName + "/logs"},
713 },
714 AppName: appName,
715 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
716 }
717 if err := s.tmplts.logs.Execute(w, data); err != nil {
718 fmt.Println(err)
719 http.Error(w, err.Error(), http.StatusInternalServerError)
720 return
721 }
722}
723
gio81246f02024-07-10 12:02:15 +0400724type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400725 Ref string `json:"ref"`
726 Repository struct {
727 Name string `json:"name"`
728 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400729 After string `json:"after"`
730 Commits []struct {
731 Id string `json:"id"`
732 Message string `json:"message"`
733 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400734}
735
gio8fae3af2024-07-25 13:43:31 +0400736func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400737 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400738 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400739 var contents strings.Builder
740 io.Copy(&contents, r.Body)
741 c := contents.String()
742 fmt.Println(c)
743 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400744 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400745 return
746 }
gio7fbd4ad2024-08-27 10:06:39 +0400747 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
748 return
749 }
750 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
751 if !ok {
752 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400753 return
754 }
gioa60f0de2024-07-08 10:49:48 +0400755 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400756 go func() {
gio11617ac2024-07-15 16:09:04 +0400757 owner, err := s.st.GetAppOwner(req.Repository.Name)
758 if err != nil {
759 return
760 }
761 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400762 if err != nil {
763 return
764 }
giof15b9da2024-09-19 06:59:16 +0400765 // TODO(gio): get only available ones by owner
766 clusters, err := s.getClusters()
767 if err != nil {
768 return
769 }
gio94904702024-07-26 16:58:34 +0400770 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
771 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
772 if err != nil {
773 return
774 }
gioe2e31e12024-08-18 08:20:56 +0400775 found := false
776 commitMsg := ""
777 for _, c := range req.Commits {
778 if c.Id == req.After {
779 found = true
780 commitMsg = c.Message
781 break
gioa60f0de2024-07-08 10:49:48 +0400782 }
783 }
gioe2e31e12024-08-18 08:20:56 +0400784 if !found {
785 fmt.Printf("Error: could not find commit message")
786 return
787 }
gio7fbd4ad2024-08-27 10:06:39 +0400788 s.l.Lock()
789 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400790 resources, err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, branch, s.getAppConfig(req.Repository.Name, branch).Namespace, networks, clusters, owner)
gio7fbd4ad2024-08-27 10:06:39 +0400791 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400792 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400793 return
794 }
gioa60f0de2024-07-08 10:49:48 +0400795 for addr, _ := range s.workers[req.Repository.Name] {
796 go func() {
797 // TODO(gio): make port configurable
798 http.Get(fmt.Sprintf("http://%s/update", addr))
799 }()
gio0eaf2712024-04-14 13:08:46 +0400800 }
801 }()
gio0eaf2712024-04-14 13:08:46 +0400802}
803
gio81246f02024-07-10 12:02:15 +0400804type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400805 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400806 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400807}
808
gio8fae3af2024-07-25 13:43:31 +0400809func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400810 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400811 vars := mux.Vars(r)
812 appName, ok := vars["app-name"]
813 if !ok || appName == "" {
814 http.Error(w, "missing app-name", http.StatusBadRequest)
815 return
816 }
gio81246f02024-07-10 12:02:15 +0400817 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400818 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
819 http.Error(w, err.Error(), http.StatusInternalServerError)
820 return
821 }
gioa60f0de2024-07-08 10:49:48 +0400822 if _, ok := s.workers[appName]; !ok {
823 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400824 }
gioa60f0de2024-07-08 10:49:48 +0400825 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400826 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400827}
828
gio11617ac2024-07-15 16:09:04 +0400829func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
830 u := r.Context().Value(userCtx)
831 if u == nil {
832 http.Error(w, "unauthorized", http.StatusUnauthorized)
833 return
834 }
835 user, ok := u.(string)
836 if !ok {
837 http.Error(w, "could not get user", http.StatusInternalServerError)
838 return
839 }
840 network := r.FormValue("network")
841 if network == "" {
842 http.Error(w, "missing network", http.StatusBadRequest)
843 return
844 }
gio5e49bb62024-07-20 10:43:19 +0400845 subdomain := r.FormValue("subdomain")
846 if subdomain == "" {
847 http.Error(w, "missing subdomain", http.StatusBadRequest)
848 return
849 }
850 appType := r.FormValue("type")
851 if appType == "" {
852 http.Error(w, "missing type", http.StatusBadRequest)
853 return
854 }
gio11617ac2024-07-15 16:09:04 +0400855 g := installer.NewFixedLengthRandomNameGenerator(3)
856 appName, err := g.Generate()
857 if err != nil {
858 http.Error(w, err.Error(), http.StatusInternalServerError)
859 return
860 }
861 if ok, err := s.client.UserExists(user); err != nil {
862 http.Error(w, err.Error(), http.StatusInternalServerError)
863 return
864 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400865 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
866 return
gio11617ac2024-07-15 16:09:04 +0400867 }
giocafd4e62024-07-31 10:53:40 +0400868 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400869 http.Error(w, err.Error(), http.StatusInternalServerError)
870 return
871 }
872 if err := s.st.CreateApp(appName, user); err != nil {
873 http.Error(w, err.Error(), http.StatusInternalServerError)
874 return
875 }
giod8ab4f52024-07-26 16:58:34 +0400876 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400877 http.Error(w, err.Error(), http.StatusInternalServerError)
878 return
879 }
880 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
881}
882
gio7fbd4ad2024-08-27 10:06:39 +0400883func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
884 u := r.Context().Value(userCtx)
885 if u == nil {
886 http.Error(w, "unauthorized", http.StatusUnauthorized)
887 return
888 }
889 user, ok := u.(string)
890 if !ok {
891 http.Error(w, "could not get user", http.StatusInternalServerError)
892 return
893 }
894 vars := mux.Vars(r)
895 appName, ok := vars["app-name"]
896 if !ok || appName == "" {
897 http.Error(w, "missing app-name", http.StatusBadRequest)
898 return
899 }
900 branch := r.FormValue("branch")
901 if branch == "" {
902 http.Error(w, "missing network", http.StatusBadRequest)
903 return
904 }
905 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
906 http.Error(w, err.Error(), http.StatusInternalServerError)
907 return
908 }
909 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
910}
911
gio81246f02024-07-10 12:02:15 +0400912type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400913 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400914 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400915 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400916 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400917}
918
gio81246f02024-07-10 12:02:15 +0400919type apiCreateAppResp struct {
920 AppName string `json:"appName"`
921 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400922}
923
gio8fae3af2024-07-25 13:43:31 +0400924func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400925 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400926 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400927 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
928 http.Error(w, err.Error(), http.StatusBadRequest)
929 return
930 }
931 g := installer.NewFixedLengthRandomNameGenerator(3)
932 appName, err := g.Generate()
933 if err != nil {
934 http.Error(w, err.Error(), http.StatusInternalServerError)
935 return
936 }
gio11617ac2024-07-15 16:09:04 +0400937 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400938 if err != nil {
gio33059762024-07-05 13:19:07 +0400939 http.Error(w, err.Error(), http.StatusInternalServerError)
940 return
941 }
gio11617ac2024-07-15 16:09:04 +0400942 if user != "" {
943 http.Error(w, "public key already registered", http.StatusBadRequest)
944 return
945 }
946 user = appName
947 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
948 http.Error(w, err.Error(), http.StatusInternalServerError)
949 return
950 }
951 password := generatePassword()
952 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
953 if err != nil {
954 http.Error(w, err.Error(), http.StatusInternalServerError)
955 return
956 }
giocafd4e62024-07-31 10:53:40 +0400957 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +0400958 http.Error(w, err.Error(), http.StatusInternalServerError)
959 return
960 }
961 if err := s.st.CreateApp(appName, user); err != nil {
962 http.Error(w, err.Error(), http.StatusInternalServerError)
963 return
964 }
giod8ab4f52024-07-26 16:58:34 +0400965 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400966 http.Error(w, err.Error(), http.StatusInternalServerError)
967 return
968 }
gio81246f02024-07-10 12:02:15 +0400969 resp := apiCreateAppResp{
970 AppName: appName,
971 Password: password,
972 }
gio33059762024-07-05 13:19:07 +0400973 if err := json.NewEncoder(w).Encode(resp); err != nil {
974 http.Error(w, err.Error(), http.StatusInternalServerError)
975 return
976 }
977}
978
giod8ab4f52024-07-26 16:58:34 +0400979func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +0400980 if !s.external {
giod8ab4f52024-07-26 16:58:34 +0400981 return true
982 }
983 for _, cfg := range s.appConfigs {
984 if strings.ToLower(cfg.Network) == network {
985 return false
986 }
987 }
988 return true
989}
990
991func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400992 s.l.Lock()
993 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400994 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400995 network = strings.ToLower(network)
996 if !s.isNetworkUseAllowed(network) {
997 return fmt.Errorf("network already used: %s", network)
998 }
gio33059762024-07-05 13:19:07 +0400999 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001000 return err
gio33059762024-07-05 13:19:07 +04001001 } else if ok {
gio11617ac2024-07-15 16:09:04 +04001002 return nil
gioa60f0de2024-07-08 10:49:48 +04001003 }
gio5e49bb62024-07-20 10:43:19 +04001004 networks, err := s.getNetworks(user)
1005 if err != nil {
1006 return err
1007 }
giod8ab4f52024-07-26 16:58:34 +04001008 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +04001009 if !ok {
1010 return fmt.Errorf("network not found: %s\n", network)
1011 }
gio33059762024-07-05 13:19:07 +04001012 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001013 return err
gio33059762024-07-05 13:19:07 +04001014 }
1015 appRepo, err := s.client.GetRepo(appName)
1016 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001017 return err
gio33059762024-07-05 13:19:07 +04001018 }
gio7fbd4ad2024-08-27 10:06:39 +04001019 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
1020 if err != nil {
1021 return err
1022 }
1023 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1024}
1025
1026func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
1027 s.l.Lock()
1028 defer s.l.Unlock()
1029 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1030 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1031 if err != nil {
1032 return err
1033 }
gioc81a8472024-09-24 13:06:19 +02001034 appCfg, err := soft.ReadFile(appRepo, "app.json")
gio7fbd4ad2024-08-27 10:06:39 +04001035 if err != nil {
1036 return err
1037 }
1038 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1039 if err != nil {
1040 return err
1041 }
gioc81a8472024-09-24 13:06:19 +02001042 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
gio7fbd4ad2024-08-27 10:06:39 +04001043}
1044
1045func (s *DodoAppServer) createAppForBranch(
1046 repo soft.RepoIO,
1047 appName string,
1048 branch string,
1049 user string,
1050 network string,
1051 files map[string][]byte,
1052) error {
1053 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1054 for path, contents := range files {
1055 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1056 return "", err
1057 }
1058 }
1059 return "init", nil
1060 }, soft.WithCommitToBranch(branch))
1061 if err != nil {
1062 return err
1063 }
1064 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001065 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001066 return err
gio33059762024-07-05 13:19:07 +04001067 }
giof15b9da2024-09-19 06:59:16 +04001068 // TODO(gio): get only available ones by owner
1069 clusters, err := s.getClusters()
1070 if err != nil {
1071 return err
1072 }
gio33059762024-07-05 13:19:07 +04001073 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001074 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1075 if err != nil {
1076 return err
1077 }
1078 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001079 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001080 return err
gio33059762024-07-05 13:19:07 +04001081 }
1082 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1083 suffix, err := suffixGen.Generate()
1084 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001085 return err
gio33059762024-07-05 13:19:07 +04001086 }
gio94904702024-07-26 16:58:34 +04001087 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001088 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001089 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001090 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001091 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001092 return err
1093 }
gio7fbd4ad2024-08-27 10:06:39 +04001094 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001095 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001096 return err
gio33059762024-07-05 13:19:07 +04001097 }
giod8ab4f52024-07-26 16:58:34 +04001098 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001099 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001100 return err
gio33059762024-07-05 13:19:07 +04001101 }
1102 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001103 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001104 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001105 return err
gio33059762024-07-05 13:19:07 +04001106 }
gio7fbd4ad2024-08-27 10:06:39 +04001107 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001108 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001109 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001110 if err != nil {
1111 return "", err
1112 }
1113 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001114 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001115 return "", err
1116 }
1117 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001118 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001119 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001120 appPath,
gio9d66f322024-07-06 13:45:10 +04001121 namespace,
1122 map[string]any{
1123 "repoAddr": s.client.GetRepoAddress(appName),
1124 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001125 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001126 "gitRepoPublicKey": s.gitRepoPublicKey,
1127 },
1128 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001129 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001130 installer.WithNoPublish(),
1131 installer.WithNoLock(),
1132 ); err != nil {
1133 return "", err
1134 }
1135 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001136 })
1137 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001138 return err
gio33059762024-07-05 13:19:07 +04001139 }
gio7fbd4ad2024-08-27 10:06:39 +04001140 return s.initAppACLs(m, appPath, appName, branch, user)
1141}
1142
1143func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
1144 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001145 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001146 return err
gio33059762024-07-05 13:19:07 +04001147 }
1148 fluxKeys, ok := cfg.Input["fluxKeys"]
1149 if !ok {
gio11617ac2024-07-15 16:09:04 +04001150 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001151 }
1152 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1153 if !ok {
gio11617ac2024-07-15 16:09:04 +04001154 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001155 }
1156 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001157 return err
gio33059762024-07-05 13:19:07 +04001158 } else if ok {
1159 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001160 return err
gio33059762024-07-05 13:19:07 +04001161 }
1162 } else {
1163 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001164 return err
gio33059762024-07-05 13:19:07 +04001165 }
1166 }
gio7fbd4ad2024-08-27 10:06:39 +04001167 if branch != "master" {
1168 return nil
1169 }
gio33059762024-07-05 13:19:07 +04001170 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001171 return err
gio33059762024-07-05 13:19:07 +04001172 }
gio7fbd4ad2024-08-27 10:06:39 +04001173 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001174 return err
gio33059762024-07-05 13:19:07 +04001175 }
gio7fbd4ad2024-08-27 10:06:39 +04001176 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 +04001177 return err
gio33059762024-07-05 13:19:07 +04001178 }
gio2ccb6e32024-08-15 12:01:33 +04001179 if !s.external {
1180 go func() {
1181 users, err := s.client.GetAllUsers()
1182 if err != nil {
1183 fmt.Println(err)
1184 return
1185 }
1186 for _, user := range users {
1187 // TODO(gio): fluxcd should have only read access
1188 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1189 fmt.Println(err)
1190 }
1191 }
1192 }()
1193 }
gio43b0f422024-08-21 10:40:13 +04001194 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1195 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001196 return nil
gio33059762024-07-05 13:19:07 +04001197}
1198
gio81246f02024-07-10 12:02:15 +04001199type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001200 User string `json:"user"`
1201 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001202}
1203
gio7fbd4ad2024-08-27 10:06:39 +04001204func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001205 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001206 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1207 http.Error(w, err.Error(), http.StatusBadRequest)
1208 return
1209 }
gio7fbd4ad2024-08-27 10:06:39 +04001210 if req.User == "" {
1211 http.Error(w, "invalid user", http.StatusBadRequest)
1212 return
1213 }
1214 if req.PublicKey == "" {
1215 http.Error(w, "invalid public key", http.StatusBadRequest)
1216 return
1217 }
1218 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001219 http.Error(w, err.Error(), http.StatusInternalServerError)
1220 return
1221 }
1222}
1223
gio94904702024-07-26 16:58:34 +04001224type dodoAppRendered struct {
1225 App struct {
1226 Ingress struct {
1227 Network string `json:"network"`
1228 Subdomain string `json:"subdomain"`
1229 } `json:"ingress"`
1230 } `json:"app"`
1231 Input struct {
1232 AppId string `json:"appId"`
1233 } `json:"input"`
1234}
1235
gio7fbd4ad2024-08-27 10:06:39 +04001236// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio43b0f422024-08-21 10:40:13 +04001237func (s *DodoAppServer) updateDodoApp(
1238 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001239 name string,
1240 branch string,
1241 namespace string,
gio43b0f422024-08-21 10:40:13 +04001242 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001243 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001244 owner string,
gio43b0f422024-08-21 10:40:13 +04001245) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001246 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001247 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001248 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001249 }
giof8843412024-05-22 16:38:05 +04001250 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001251 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001252 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001253 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001254 }
gioc81a8472024-09-24 13:06:19 +02001255 appCfg, err := soft.ReadFile(repo, "app.json")
gio0eaf2712024-04-14 13:08:46 +04001256 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001257 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001258 }
1259 app, err := installer.NewDodoApp(appCfg)
1260 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001261 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001262 }
giof8843412024-05-22 16:38:05 +04001263 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001264 var ret installer.ReleaseResources
1265 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1266 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001267 app,
1268 "app",
1269 "/.dodo/app",
1270 namespace,
1271 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001272 "repoAddr": repo.FullAddress(),
1273 "repoPublicAddr": s.repoPublicAddr,
1274 "managerAddr": fmt.Sprintf("http://%s", s.self),
1275 "appId": name,
1276 "branch": branch,
1277 "sshPrivateKey": s.sshKey,
1278 "username": owner,
gio94904702024-07-26 16:58:34 +04001279 },
1280 installer.WithNoPull(),
1281 installer.WithNoPublish(),
1282 installer.WithConfig(&s.env),
1283 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001284 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001285 installer.WithLocalChartGenerator(lg),
1286 installer.WithNoLock(),
1287 )
1288 if err != nil {
1289 return "", err
1290 }
1291 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001292 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001293 return "", nil
1294 }
1295 if _, err := m.Install(
1296 appStatus,
1297 "status",
1298 "/.dodo/status",
1299 s.namespace,
1300 map[string]any{
1301 "appName": rendered.Input.AppId,
1302 "network": rendered.App.Ingress.Network,
1303 "appSubdomain": rendered.App.Ingress.Subdomain,
1304 },
1305 installer.WithNoPull(),
1306 installer.WithNoPublish(),
1307 installer.WithConfig(&s.env),
1308 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001309 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001310 installer.WithLocalChartGenerator(lg),
1311 installer.WithNoLock(),
1312 ); err != nil {
1313 return "", err
1314 }
1315 return "install app", nil
1316 },
gio7fbd4ad2024-08-27 10:06:39 +04001317 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001318 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001319 ); err != nil {
1320 return installer.ReleaseResources{}, err
1321 }
gio43b0f422024-08-21 10:40:13 +04001322 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1323 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001324 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001325}
gio33059762024-07-05 13:19:07 +04001326
gio7fbd4ad2024-08-27 10:06:39 +04001327func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001328 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001329 appTmpl, err := s.appTmpls.Find(appType)
1330 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001331 return nil, err
gio33059762024-07-05 13:19:07 +04001332 }
gioc81a8472024-09-24 13:06:19 +02001333 return appTmpl.Render(fmt.Sprintf("%s/stat/schemas/dodo_app.jsonschema", s.selfPublic), network, subdomain)
gio33059762024-07-05 13:19:07 +04001334}
gio81246f02024-07-10 12:02:15 +04001335
1336func generatePassword() string {
1337 return "foo"
1338}
giocb34ad22024-07-11 08:01:13 +04001339
gio11617ac2024-07-15 16:09:04 +04001340func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001341 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001342 resp, err := http.Get(addr)
1343 if err != nil {
1344 return nil, err
1345 }
gio23bdc1b2024-07-11 16:07:47 +04001346 networks := []installer.Network{}
1347 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001348 return nil, err
1349 }
gio11617ac2024-07-15 16:09:04 +04001350 return s.nf.Filter(user, networks)
1351}
1352
giof15b9da2024-09-19 06:59:16 +04001353func (s *DodoAppServer) getClusters() ([]installer.Cluster, error) {
1354 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1355 resp, err := http.Get(addr)
1356 if err != nil {
1357 return nil, err
1358 }
1359 clusters := []installer.Cluster{}
1360 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1361 return nil, err
1362 }
1363 fmt.Printf("CLUSTERS %+v\n", clusters)
1364 return clusters, nil
1365}
1366
gio8fae3af2024-07-25 13:43:31 +04001367type publicNetworkData struct {
1368 Name string `json:"name"`
1369 Domain string `json:"domain"`
1370}
1371
1372type publicData struct {
1373 Networks []publicNetworkData `json:"networks"`
1374 Types []string `json:"types"`
1375}
1376
1377func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001378 w.Header().Set("Access-Control-Allow-Origin", "*")
1379 s.l.Lock()
1380 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001381 networks, err := s.getNetworks("")
1382 if err != nil {
1383 http.Error(w, err.Error(), http.StatusInternalServerError)
1384 return
1385 }
1386 var ret publicData
1387 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001388 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1389 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1390 }
gio8fae3af2024-07-25 13:43:31 +04001391 }
1392 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001393 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001394 }
gio8fae3af2024-07-25 13:43:31 +04001395 if err := json.NewEncoder(w).Encode(ret); err != nil {
1396 http.Error(w, err.Error(), http.StatusInternalServerError)
1397 return
1398 }
1399}
1400
gio7fbd4ad2024-08-27 10:06:39 +04001401func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001402 if err != nil {
1403 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001404 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001405 fmt.Printf("Error: %s\n", err.Error())
1406 return err
1407 }
1408 return err
1409 }
1410 var resB bytes.Buffer
1411 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001412 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001413 fmt.Printf("Error: %s\n", err.Error())
1414 return err
1415 }
1416 return err
1417 }
gio7fbd4ad2024-08-27 10:06:39 +04001418 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001419 fmt.Printf("Error: %s\n", err.Error())
1420 return err
1421 }
1422 return nil
1423}
1424
gio11617ac2024-07-15 16:09:04 +04001425func pickNetwork(networks []installer.Network, network string) []installer.Network {
1426 for _, n := range networks {
1427 if n.Name == network {
1428 return []installer.Network{n}
1429 }
1430 }
1431 return []installer.Network{}
1432}
1433
1434type NetworkFilter interface {
1435 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1436}
1437
1438type noNetworkFilter struct{}
1439
1440func NewNoNetworkFilter() NetworkFilter {
1441 return noNetworkFilter{}
1442}
1443
gio8fae3af2024-07-25 13:43:31 +04001444func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001445 return networks, nil
1446}
1447
1448type filterByOwner struct {
1449 st Store
1450}
1451
1452func NewNetworkFilterByOwner(st Store) NetworkFilter {
1453 return &filterByOwner{st}
1454}
1455
1456func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001457 if user == "" {
1458 return networks, nil
1459 }
gio11617ac2024-07-15 16:09:04 +04001460 network, err := f.st.GetUserNetwork(user)
1461 if err != nil {
1462 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001463 }
1464 ret := []installer.Network{}
1465 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001466 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001467 ret = append(ret, n)
1468 }
1469 }
giocb34ad22024-07-11 08:01:13 +04001470 return ret, nil
1471}
gio11617ac2024-07-15 16:09:04 +04001472
1473type allowListFilter struct {
1474 allowed []string
1475}
1476
1477func NewAllowListFilter(allowed []string) NetworkFilter {
1478 return &allowListFilter{allowed}
1479}
1480
gio8fae3af2024-07-25 13:43:31 +04001481func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001482 ret := []installer.Network{}
1483 for _, n := range networks {
1484 if slices.Contains(f.allowed, n.Name) {
1485 ret = append(ret, n)
1486 }
1487 }
1488 return ret, nil
1489}
1490
1491type combinedNetworkFilter struct {
1492 filters []NetworkFilter
1493}
1494
1495func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1496 return &combinedNetworkFilter{filters}
1497}
1498
gio8fae3af2024-07-25 13:43:31 +04001499func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001500 ret := networks
1501 var err error
1502 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001503 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001504 if err != nil {
1505 return nil, err
1506 }
1507 }
1508 return ret, nil
1509}
giocafd4e62024-07-31 10:53:40 +04001510
1511type user struct {
1512 Username string `json:"username"`
1513 Email string `json:"email"`
1514 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1515}
1516
1517func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1518 go s.syncUsers()
1519}
1520
1521func (s *DodoAppServer) syncUsers() {
1522 if s.external {
1523 panic("MUST NOT REACH!")
1524 }
1525 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1526 if err != nil {
1527 return
1528 }
1529 users := []user{}
1530 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1531 fmt.Println(err)
1532 return
1533 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001534 validUsernames := make(map[string]user)
1535 for _, u := range users {
1536 validUsernames[u.Username] = u
1537 }
1538 allClientUsers, err := s.client.GetAllUsers()
1539 if err != nil {
1540 fmt.Println(err)
1541 return
1542 }
1543 keyToUser := make(map[string]string)
1544 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001545 if clientUser == "admin" || clientUser == "fluxcd" {
1546 continue
1547 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001548 userData, ok := validUsernames[clientUser]
1549 if !ok {
1550 if err := s.client.RemoveUser(clientUser); err != nil {
1551 fmt.Println(err)
1552 return
1553 }
1554 } else {
1555 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1556 if err != nil {
1557 fmt.Println(err)
1558 return
1559 }
1560 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001561 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001562 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001563 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001564 })
1565 if !keyOk {
1566 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1567 fmt.Println(err)
1568 }
1569 } else {
1570 keyToUser[cleanKey] = clientUser
1571 }
1572 }
1573 }
1574 }
giocafd4e62024-07-31 10:53:40 +04001575 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001576 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1577 fmt.Println(err)
1578 return
1579 }
giocafd4e62024-07-31 10:53:40 +04001580 if len(u.SSHPublicKeys) == 0 {
1581 continue
1582 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001583 ok, err := s.client.UserExists(u.Username)
1584 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001585 fmt.Println(err)
1586 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001587 }
1588 if !ok {
1589 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1590 fmt.Println(err)
1591 return
1592 }
1593 } else {
1594 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001595 cleanKey := soft.CleanKey(key)
1596 if user, ok := keyToUser[cleanKey]; ok {
1597 if u.Username != user {
1598 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1599 }
1600 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001601 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001602 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001603 fmt.Println(err)
1604 return
giocafd4e62024-07-31 10:53:40 +04001605 }
1606 }
1607 }
1608 }
1609 repos, err := s.client.GetAllRepos()
1610 if err != nil {
1611 return
1612 }
1613 for _, r := range repos {
1614 if r == ConfigRepoName {
1615 continue
1616 }
1617 for _, u := range users {
1618 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1619 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001620 continue
giocafd4e62024-07-31 10:53:40 +04001621 }
1622 }
1623 }
1624}
giob4a3a192024-08-19 09:55:47 +04001625
1626func extractResourceData(resources []installer.Resource) (resourceData, error) {
1627 var ret resourceData
1628 for _, r := range resources {
1629 t, ok := r.Annotations["dodo.cloud/resource-type"]
1630 if !ok {
1631 continue
1632 }
1633 switch t {
1634 case "volume":
1635 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1636 if !ok {
1637 return resourceData{}, fmt.Errorf("no name")
1638 }
1639 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1640 if !ok {
1641 return resourceData{}, fmt.Errorf("no size")
1642 }
1643 ret.Volume = append(ret.Volume, volume{name, size})
1644 case "postgresql":
1645 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1646 if !ok {
1647 return resourceData{}, fmt.Errorf("no name")
1648 }
1649 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1650 if !ok {
1651 return resourceData{}, fmt.Errorf("no version")
1652 }
1653 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1654 if !ok {
1655 return resourceData{}, fmt.Errorf("no volume")
1656 }
1657 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1658 case "ingress":
1659 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1660 if !ok {
1661 return resourceData{}, fmt.Errorf("no host")
1662 }
1663 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001664 case "virtual-machine":
1665 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1666 if !ok {
1667 return resourceData{}, fmt.Errorf("no name")
1668 }
1669 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1670 if !ok {
1671 return resourceData{}, fmt.Errorf("no user")
1672 }
1673 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1674 if !ok {
1675 return resourceData{}, fmt.Errorf("no cpu cores")
1676 }
1677 cpuCores, err := strconv.Atoi(cpuCoresS)
1678 if err != nil {
1679 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1680 }
1681 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1682 if !ok {
1683 return resourceData{}, fmt.Errorf("no memory")
1684 }
1685 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001686 default:
1687 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1688 }
1689 }
1690 return ret, nil
1691}
gio7fbd4ad2024-08-27 10:06:39 +04001692
1693func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
gioc81a8472024-09-24 13:06:19 +02001694 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
1695 "app.cue": from,
1696 })
gio7fbd4ad2024-08-27 10:06:39 +04001697 if err != nil {
1698 return "", nil, err
1699 }
1700 if err := cfg.Err(); err != nil {
1701 return "", nil, err
1702 }
1703 if err := cfg.Validate(); err != nil {
1704 return "", nil, err
1705 }
1706 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1707 if err := subdomain.Err(); err != nil {
1708 return "", nil, err
1709 }
1710 subdomainStr, err := subdomain.String()
1711 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1712 if err := network.Err(); err != nil {
1713 return "", nil, err
1714 }
1715 networkStr, err := network.String()
1716 if err != nil {
1717 return "", nil, err
1718 }
1719 newCfg := map[string]any{}
1720 if err := cfg.Decode(&newCfg); err != nil {
1721 return "", nil, err
1722 }
1723 app, ok := newCfg["app"].(map[string]any)
1724 if !ok {
1725 return "", nil, fmt.Errorf("not a map")
1726 }
1727 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1728 app["dev"] = map[string]any{
1729 "enabled": true,
1730 "username": username,
1731 }
1732 buf, err := json.MarshalIndent(newCfg, "", "\t")
1733 if err != nil {
1734 return "", nil, err
1735 }
1736 return networkStr, buf, nil
1737}