blob: 00cf794d5e8f2f484df0d8888a3dcd8ffdcf9177 [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)
gio5887caa2024-10-03 15:07:23 +0400239 r.HandleFunc("/{app-name}/branch/{branch}/delete", s.handleBranchDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400240 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400241 r.HandleFunc("/{app-name}/delete", s.handleAppDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400242 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400243 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400244 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
245 }()
246 go func() {
247 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400248 r.HandleFunc("/update", s.handleAPIUpdate)
249 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400250 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400251 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400252 if !s.external {
253 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
254 }
gioa60f0de2024-07-08 10:49:48 +0400255 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
256 }()
giocafd4e62024-07-31 10:53:40 +0400257 if !s.external {
258 go func() {
259 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400260 for {
261 delay := time.Duration(rand.Intn(60)+60) * time.Second
262 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400263 s.syncUsers()
264 }
265 }()
266 }
gioa60f0de2024-07-08 10:49:48 +0400267 return <-e
268}
269
gio11617ac2024-07-15 16:09:04 +0400270type UserGetter interface {
271 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400272 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400273}
274
275type externalUserGetter struct {
276 sc *securecookie.SecureCookie
277}
278
279func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400280 return &externalUserGetter{securecookie.New(
281 securecookie.GenerateRandomKey(64),
282 securecookie.GenerateRandomKey(32),
283 )}
gio11617ac2024-07-15 16:09:04 +0400284}
285
286func (ug *externalUserGetter) Get(r *http.Request) string {
287 cookie, err := r.Cookie(sessionCookie)
288 if err != nil {
289 return ""
290 }
291 var user string
292 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
293 return ""
294 }
295 return user
296}
297
gio8fae3af2024-07-25 13:43:31 +0400298func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
299 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
300 cookie := &http.Cookie{
301 Name: sessionCookie,
302 Value: encoded,
303 Path: "/",
304 Secure: true,
305 HttpOnly: true,
306 }
307 http.SetCookie(w, cookie)
308 return nil
309 } else {
310 return err
311 }
312}
313
gio11617ac2024-07-15 16:09:04 +0400314type internalUserGetter struct{}
315
316func NewInternalUserGetter() UserGetter {
317 return internalUserGetter{}
318}
319
320func (ug internalUserGetter) Get(r *http.Request) string {
giodd213152024-09-27 11:26:59 +0200321 return r.Header.Get("X-Forwarded-User")
gio11617ac2024-07-15 16:09:04 +0400322}
323
gio8fae3af2024-07-25 13:43:31 +0400324func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
325 return nil
326}
327
gio81246f02024-07-10 12:02:15 +0400328func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
329 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400330 if strings.HasSuffix(r.URL.Path, loginPath) ||
331 strings.HasPrefix(r.URL.Path, logoutPath) ||
332 strings.HasPrefix(r.URL.Path, staticPath) ||
gioc81a8472024-09-24 13:06:19 +0200333 strings.HasPrefix(r.URL.Path, schemasPath) ||
gio8fae3af2024-07-25 13:43:31 +0400334 strings.HasPrefix(r.URL.Path, apiPublicData) ||
335 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400336 next.ServeHTTP(w, r)
337 return
338 }
gio11617ac2024-07-15 16:09:04 +0400339 user := s.ug.Get(r)
340 if user == "" {
gio81246f02024-07-10 12:02:15 +0400341 vars := mux.Vars(r)
342 appName, ok := vars["app-name"]
343 if !ok || appName == "" {
344 http.Error(w, "missing app-name", http.StatusBadRequest)
345 return
346 }
347 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
348 return
349 }
gio81246f02024-07-10 12:02:15 +0400350 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
351 })
352}
353
gioc81a8472024-09-24 13:06:19 +0200354func (s *DodoAppServer) handleSchema(w http.ResponseWriter, r *http.Request) {
355 w.Header().Set("Content-Type", "application/schema+json")
356 w.Write(dodoAppJsonSchema)
357}
358
gio81246f02024-07-10 12:02:15 +0400359func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400360 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400361 http.SetCookie(w, &http.Cookie{
362 Name: sessionCookie,
363 Value: "",
364 Path: "/",
365 HttpOnly: true,
366 Secure: true,
367 })
368 http.Redirect(w, r, "/", http.StatusSeeOther)
369}
370
371func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
372 vars := mux.Vars(r)
373 appName, ok := vars["app-name"]
374 if !ok || appName == "" {
375 http.Error(w, "missing app-name", http.StatusBadRequest)
376 return
377 }
378 fmt.Fprint(w, `
379<!DOCTYPE html>
380<html lang='en'>
381 <head>
382 <title>dodo: app - login</title>
383 <meta charset='utf-8'>
384 </head>
385 <body>
386 <form action="" method="POST">
387 <input type="password" placeholder="Password" name="password" required />
388 <button type="submit">Login</button>
389 </form>
390 </body>
391</html>
392`)
393}
394
395func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
396 vars := mux.Vars(r)
397 appName, ok := vars["app-name"]
398 if !ok || appName == "" {
399 http.Error(w, "missing app-name", http.StatusBadRequest)
400 return
401 }
402 password := r.FormValue("password")
403 if password == "" {
404 http.Error(w, "missing password", http.StatusBadRequest)
405 return
406 }
407 user, err := s.st.GetAppOwner(appName)
408 if err != nil {
409 http.Error(w, err.Error(), http.StatusInternalServerError)
410 return
411 }
412 hashed, err := s.st.GetUserPassword(user)
413 if err != nil {
414 http.Error(w, err.Error(), http.StatusInternalServerError)
415 return
416 }
417 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
418 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
419 return
420 }
gio8fae3af2024-07-25 13:43:31 +0400421 if err := s.ug.Encode(w, user); err != nil {
422 http.Error(w, err.Error(), http.StatusInternalServerError)
423 return
gio81246f02024-07-10 12:02:15 +0400424 }
425 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
426}
427
giob4a3a192024-08-19 09:55:47 +0400428type navItem struct {
429 Name string
430 Address string
431}
432
gio23bdc1b2024-07-11 16:07:47 +0400433type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400434 Navigation []navItem
435 Apps []string
436 Networks []installer.Network
437 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400438}
439
gioa60f0de2024-07-08 10:49:48 +0400440func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400441 user := r.Context().Value(userCtx)
442 if user == nil {
443 http.Error(w, "unauthorized", http.StatusUnauthorized)
444 return
445 }
446 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400447 if err != nil {
448 http.Error(w, err.Error(), http.StatusInternalServerError)
449 return
450 }
gio11617ac2024-07-15 16:09:04 +0400451 networks, err := s.getNetworks(user.(string))
452 if err != nil {
453 http.Error(w, err.Error(), http.StatusInternalServerError)
454 return
455 }
giob54db242024-07-30 18:49:33 +0400456 var types []string
457 for _, t := range s.appTmpls.Types() {
458 types = append(types, strings.Replace(t, "-", ":", 1))
459 }
giob4a3a192024-08-19 09:55:47 +0400460 n := []navItem{navItem{"Home", "/"}}
461 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400462 if err := s.tmplts.index.Execute(w, data); err != nil {
463 http.Error(w, err.Error(), http.StatusInternalServerError)
464 return
gioa60f0de2024-07-08 10:49:48 +0400465 }
466}
467
gio5e49bb62024-07-20 10:43:19 +0400468type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400469 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400470 Name string
gio5887caa2024-10-03 15:07:23 +0400471 Branch string
gio5e49bb62024-07-20 10:43:19 +0400472 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400473 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400474 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400475 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400476}
477
gioa60f0de2024-07-08 10:49:48 +0400478func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
479 vars := mux.Vars(r)
480 appName, ok := vars["app-name"]
481 if !ok || appName == "" {
482 http.Error(w, "missing app-name", http.StatusBadRequest)
483 return
484 }
gio7fbd4ad2024-08-27 10:06:39 +0400485 branch, ok := vars["branch"]
486 if !ok || branch == "" {
487 branch = "master"
488 }
gio94904702024-07-26 16:58:34 +0400489 u := r.Context().Value(userCtx)
490 if u == nil {
491 http.Error(w, "unauthorized", http.StatusUnauthorized)
492 return
493 }
494 user, ok := u.(string)
495 if !ok {
496 http.Error(w, "could not get user", http.StatusInternalServerError)
497 return
498 }
499 owner, err := s.st.GetAppOwner(appName)
500 if err != nil {
501 http.Error(w, err.Error(), http.StatusInternalServerError)
502 return
503 }
504 if owner != user {
505 http.Error(w, "unauthorized", http.StatusUnauthorized)
506 return
507 }
gio7fbd4ad2024-08-27 10:06:39 +0400508 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400509 if err != nil {
510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return
512 }
gio183e8342024-08-20 06:01:24 +0400513 var lastCommitResources resourceData
514 if len(commits) > 0 {
515 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
516 if err != nil {
517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
519 }
520 r, err := extractResourceData(lastCommit.Resources.Helm)
521 if err != nil {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
524 }
525 lastCommitResources = r
526 }
gio7fbd4ad2024-08-27 10:06:39 +0400527 branches, err := s.st.GetBranches(appName)
528 if err != nil {
529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
gio5e49bb62024-07-20 10:43:19 +0400532 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400533 Navigation: []navItem{
534 navItem{"Home", "/"},
535 navItem{appName, "/" + appName},
536 },
gio5e49bb62024-07-20 10:43:19 +0400537 Name: appName,
gio5887caa2024-10-03 15:07:23 +0400538 Branch: branch,
gio5e49bb62024-07-20 10:43:19 +0400539 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
540 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400541 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400542 Branches: branches,
543 }
544 if branch != "master" {
545 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400546 }
547 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
gioa60f0de2024-07-08 10:49:48 +0400550 }
gio0eaf2712024-04-14 13:08:46 +0400551}
552
giocfb228c2024-09-06 15:44:31 +0400553type appEnv struct {
554 Profile string `json:"envProfile"`
555}
556
557func (s *DodoAppServer) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
558 vars := mux.Vars(r)
559 appName, ok := vars["app-name"]
560 if !ok || appName == "" {
561 http.Error(w, "missing app-name", http.StatusBadRequest)
562 return
563 }
564 branch, ok := vars["branch"]
565 if !ok || branch == "" {
566 branch = "master"
567 }
568 info, err := s.st.GetLastCommitInfo(appName, branch)
569 if err != nil {
570 http.Error(w, err.Error(), http.StatusInternalServerError)
571 return
572 }
573 var e appEnv
574 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
578 fmt.Fprintln(w, e.Profile)
579}
580
giob4a3a192024-08-19 09:55:47 +0400581type volume struct {
582 Name string
583 Size string
584}
585
586type postgresql struct {
587 Name string
588 Version string
589 Volume string
590}
591
592type ingress struct {
593 Host string
594}
595
gio7fbd4ad2024-08-27 10:06:39 +0400596type vm struct {
597 Name string
598 User string
599 CPUCores int
600 Memory string
601}
602
giob4a3a192024-08-19 09:55:47 +0400603type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400604 Volume []volume
605 PostgreSQL []postgresql
606 Ingress []ingress
607 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400608}
609
610type commitStatusData struct {
611 Navigation []navItem
612 AppName string
613 Commit Commit
614 Resources resourceData
615}
616
617func (s *DodoAppServer) handleAppCommit(w http.ResponseWriter, r *http.Request) {
618 vars := mux.Vars(r)
619 appName, ok := vars["app-name"]
620 if !ok || appName == "" {
621 http.Error(w, "missing app-name", http.StatusBadRequest)
622 return
623 }
624 hash, ok := vars["hash"]
625 if !ok || appName == "" {
626 http.Error(w, "missing app-name", http.StatusBadRequest)
627 return
628 }
629 u := r.Context().Value(userCtx)
630 if u == nil {
631 http.Error(w, "unauthorized", http.StatusUnauthorized)
632 return
633 }
634 user, ok := u.(string)
635 if !ok {
636 http.Error(w, "could not get user", http.StatusInternalServerError)
637 return
638 }
639 owner, err := s.st.GetAppOwner(appName)
640 if err != nil {
641 http.Error(w, err.Error(), http.StatusInternalServerError)
642 return
643 }
644 if owner != user {
645 http.Error(w, "unauthorized", http.StatusUnauthorized)
646 return
647 }
648 commit, err := s.st.GetCommit(hash)
649 if err != nil {
650 // TODO(gio): not-found ?
651 http.Error(w, err.Error(), http.StatusInternalServerError)
652 return
653 }
654 var res strings.Builder
655 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
656 http.Error(w, err.Error(), http.StatusInternalServerError)
657 return
658 }
659 resData, err := extractResourceData(commit.Resources.Helm)
660 if err != nil {
661 http.Error(w, err.Error(), http.StatusInternalServerError)
662 return
663 }
664 data := commitStatusData{
665 Navigation: []navItem{
666 navItem{"Home", "/"},
667 navItem{appName, "/" + appName},
668 navItem{hash, "/" + appName + "/" + hash},
669 },
670 AppName: appName,
671 Commit: commit,
672 Resources: resData,
673 }
674 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
675 http.Error(w, err.Error(), http.StatusInternalServerError)
676 return
677 }
678}
679
gio183e8342024-08-20 06:01:24 +0400680type logData struct {
681 Navigation []navItem
682 AppName string
683 Logs template.HTML
684}
685
686func (s *DodoAppServer) handleAppLogs(w http.ResponseWriter, r *http.Request) {
687 vars := mux.Vars(r)
688 appName, ok := vars["app-name"]
689 if !ok || appName == "" {
690 http.Error(w, "missing app-name", http.StatusBadRequest)
691 return
692 }
693 u := r.Context().Value(userCtx)
694 if u == nil {
695 http.Error(w, "unauthorized", http.StatusUnauthorized)
696 return
697 }
698 user, ok := u.(string)
699 if !ok {
700 http.Error(w, "could not get user", http.StatusInternalServerError)
701 return
702 }
703 owner, err := s.st.GetAppOwner(appName)
704 if err != nil {
705 http.Error(w, err.Error(), http.StatusInternalServerError)
706 return
707 }
708 if owner != user {
709 http.Error(w, "unauthorized", http.StatusUnauthorized)
710 return
711 }
712 data := logData{
713 Navigation: []navItem{
714 navItem{"Home", "/"},
715 navItem{appName, "/" + appName},
716 navItem{"Logs", "/" + appName + "/logs"},
717 },
718 AppName: appName,
719 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
720 }
721 if err := s.tmplts.logs.Execute(w, data); err != nil {
722 fmt.Println(err)
723 http.Error(w, err.Error(), http.StatusInternalServerError)
724 return
725 }
726}
727
gio81246f02024-07-10 12:02:15 +0400728type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400729 Ref string `json:"ref"`
730 Repository struct {
731 Name string `json:"name"`
732 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400733 After string `json:"after"`
734 Commits []struct {
735 Id string `json:"id"`
736 Message string `json:"message"`
737 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400738}
739
gio8fae3af2024-07-25 13:43:31 +0400740func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400741 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400742 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400743 var contents strings.Builder
744 io.Copy(&contents, r.Body)
745 c := contents.String()
746 fmt.Println(c)
747 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400748 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400749 return
750 }
gio7fbd4ad2024-08-27 10:06:39 +0400751 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
752 return
753 }
754 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
755 if !ok {
756 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400757 return
758 }
gioa60f0de2024-07-08 10:49:48 +0400759 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400760 go func() {
gio11617ac2024-07-15 16:09:04 +0400761 owner, err := s.st.GetAppOwner(req.Repository.Name)
762 if err != nil {
763 return
764 }
765 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400766 if err != nil {
767 return
768 }
giof15b9da2024-09-19 06:59:16 +0400769 // TODO(gio): get only available ones by owner
770 clusters, err := s.getClusters()
771 if err != nil {
772 return
773 }
gio94904702024-07-26 16:58:34 +0400774 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
775 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
776 if err != nil {
777 return
778 }
gioe2e31e12024-08-18 08:20:56 +0400779 found := false
780 commitMsg := ""
781 for _, c := range req.Commits {
782 if c.Id == req.After {
783 found = true
784 commitMsg = c.Message
785 break
gioa60f0de2024-07-08 10:49:48 +0400786 }
787 }
gioe2e31e12024-08-18 08:20:56 +0400788 if !found {
789 fmt.Printf("Error: could not find commit message")
790 return
791 }
gio7fbd4ad2024-08-27 10:06:39 +0400792 s.l.Lock()
793 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400794 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 +0400795 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400796 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400797 return
798 }
gioa60f0de2024-07-08 10:49:48 +0400799 for addr, _ := range s.workers[req.Repository.Name] {
800 go func() {
801 // TODO(gio): make port configurable
802 http.Get(fmt.Sprintf("http://%s/update", addr))
803 }()
gio0eaf2712024-04-14 13:08:46 +0400804 }
805 }()
gio0eaf2712024-04-14 13:08:46 +0400806}
807
gio81246f02024-07-10 12:02:15 +0400808type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400809 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400810 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400811}
812
gio8fae3af2024-07-25 13:43:31 +0400813func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400814 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400815 vars := mux.Vars(r)
816 appName, ok := vars["app-name"]
817 if !ok || appName == "" {
818 http.Error(w, "missing app-name", http.StatusBadRequest)
819 return
820 }
gio81246f02024-07-10 12:02:15 +0400821 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400822 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
823 http.Error(w, err.Error(), http.StatusInternalServerError)
824 return
825 }
gioa60f0de2024-07-08 10:49:48 +0400826 if _, ok := s.workers[appName]; !ok {
827 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400828 }
gioa60f0de2024-07-08 10:49:48 +0400829 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400830 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400831}
832
gio11617ac2024-07-15 16:09:04 +0400833func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
834 u := r.Context().Value(userCtx)
835 if u == nil {
836 http.Error(w, "unauthorized", http.StatusUnauthorized)
837 return
838 }
839 user, ok := u.(string)
840 if !ok {
841 http.Error(w, "could not get user", http.StatusInternalServerError)
842 return
843 }
844 network := r.FormValue("network")
845 if network == "" {
846 http.Error(w, "missing network", http.StatusBadRequest)
847 return
848 }
gio5e49bb62024-07-20 10:43:19 +0400849 subdomain := r.FormValue("subdomain")
850 if subdomain == "" {
851 http.Error(w, "missing subdomain", http.StatusBadRequest)
852 return
853 }
854 appType := r.FormValue("type")
855 if appType == "" {
856 http.Error(w, "missing type", http.StatusBadRequest)
857 return
858 }
gio5cc6afc2024-10-06 09:33:44 +0400859 appName := r.FormValue("name")
860 var err error
861 if appName == "" {
862 g := installer.NewFixedLengthRandomNameGenerator(3)
863 appName, err = g.Generate()
864 }
gio11617ac2024-07-15 16:09:04 +0400865 if err != nil {
866 http.Error(w, err.Error(), http.StatusInternalServerError)
867 return
868 }
869 if ok, err := s.client.UserExists(user); err != nil {
870 http.Error(w, err.Error(), http.StatusInternalServerError)
871 return
872 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400873 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
874 return
gio11617ac2024-07-15 16:09:04 +0400875 }
giocafd4e62024-07-31 10:53:40 +0400876 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400877 http.Error(w, err.Error(), http.StatusInternalServerError)
878 return
879 }
880 if err := s.st.CreateApp(appName, user); err != nil {
881 http.Error(w, err.Error(), http.StatusInternalServerError)
882 return
883 }
giod8ab4f52024-07-26 16:58:34 +0400884 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400885 http.Error(w, err.Error(), http.StatusInternalServerError)
886 return
887 }
888 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
889}
890
gio7fbd4ad2024-08-27 10:06:39 +0400891func (s *DodoAppServer) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
892 u := r.Context().Value(userCtx)
893 if u == nil {
894 http.Error(w, "unauthorized", http.StatusUnauthorized)
895 return
896 }
897 user, ok := u.(string)
898 if !ok {
899 http.Error(w, "could not get user", http.StatusInternalServerError)
900 return
901 }
902 vars := mux.Vars(r)
903 appName, ok := vars["app-name"]
904 if !ok || appName == "" {
905 http.Error(w, "missing app-name", http.StatusBadRequest)
906 return
907 }
908 branch := r.FormValue("branch")
909 if branch == "" {
gio5887caa2024-10-03 15:07:23 +0400910 http.Error(w, "missing branch", http.StatusBadRequest)
gio7fbd4ad2024-08-27 10:06:39 +0400911 return
912 }
913 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
914 http.Error(w, err.Error(), http.StatusInternalServerError)
915 return
916 }
917 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
918}
919
gio5887caa2024-10-03 15:07:23 +0400920func (s *DodoAppServer) handleBranchDelete(w http.ResponseWriter, r *http.Request) {
921 u := r.Context().Value(userCtx)
922 if u == nil {
923 http.Error(w, "unauthorized", http.StatusUnauthorized)
924 return
925 }
926 vars := mux.Vars(r)
927 appName, ok := vars["app-name"]
928 if !ok || appName == "" {
929 http.Error(w, "missing app-name", http.StatusBadRequest)
930 return
931 }
932 branch, ok := vars["branch"]
933 if !ok || branch == "" {
934 http.Error(w, "missing branch", http.StatusBadRequest)
935 return
936 }
937 if err := s.deleteBranch(appName, branch); err != nil {
938 http.Error(w, err.Error(), http.StatusInternalServerError)
939 return
940 }
941 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
942}
943
944func (s *DodoAppServer) handleAppDelete(w http.ResponseWriter, r *http.Request) {
945 u := r.Context().Value(userCtx)
946 if u == nil {
947 http.Error(w, "unauthorized", http.StatusUnauthorized)
948 return
949 }
950 vars := mux.Vars(r)
951 appName, ok := vars["app-name"]
952 if !ok || appName == "" {
953 http.Error(w, "missing app-name", http.StatusBadRequest)
954 return
955 }
956 if err := s.deleteApp(appName); err != nil {
957 http.Error(w, err.Error(), http.StatusInternalServerError)
958 return
959 }
gioe44c1512024-10-06 14:13:55 +0400960 http.Redirect(w, r, "/", http.StatusSeeOther)
gio5887caa2024-10-03 15:07:23 +0400961}
962
gio81246f02024-07-10 12:02:15 +0400963type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400964 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400965 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400966 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400967 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400968}
969
gio81246f02024-07-10 12:02:15 +0400970type apiCreateAppResp struct {
971 AppName string `json:"appName"`
972 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400973}
974
gio8fae3af2024-07-25 13:43:31 +0400975func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400976 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400977 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400978 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
979 http.Error(w, err.Error(), http.StatusBadRequest)
980 return
981 }
982 g := installer.NewFixedLengthRandomNameGenerator(3)
983 appName, err := g.Generate()
984 if err != nil {
985 http.Error(w, err.Error(), http.StatusInternalServerError)
986 return
987 }
gio11617ac2024-07-15 16:09:04 +0400988 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400989 if err != nil {
gio33059762024-07-05 13:19:07 +0400990 http.Error(w, err.Error(), http.StatusInternalServerError)
991 return
992 }
gio11617ac2024-07-15 16:09:04 +0400993 if user != "" {
994 http.Error(w, "public key already registered", http.StatusBadRequest)
995 return
996 }
997 user = appName
998 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
999 http.Error(w, err.Error(), http.StatusInternalServerError)
1000 return
1001 }
1002 password := generatePassword()
1003 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1004 if err != nil {
1005 http.Error(w, err.Error(), http.StatusInternalServerError)
1006 return
1007 }
giocafd4e62024-07-31 10:53:40 +04001008 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +04001009 http.Error(w, err.Error(), http.StatusInternalServerError)
1010 return
1011 }
1012 if err := s.st.CreateApp(appName, user); err != nil {
1013 http.Error(w, err.Error(), http.StatusInternalServerError)
1014 return
1015 }
giod8ab4f52024-07-26 16:58:34 +04001016 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +04001017 http.Error(w, err.Error(), http.StatusInternalServerError)
1018 return
1019 }
gio81246f02024-07-10 12:02:15 +04001020 resp := apiCreateAppResp{
1021 AppName: appName,
1022 Password: password,
1023 }
gio33059762024-07-05 13:19:07 +04001024 if err := json.NewEncoder(w).Encode(resp); err != nil {
1025 http.Error(w, err.Error(), http.StatusInternalServerError)
1026 return
1027 }
1028}
1029
giod8ab4f52024-07-26 16:58:34 +04001030func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +04001031 if !s.external {
giod8ab4f52024-07-26 16:58:34 +04001032 return true
1033 }
1034 for _, cfg := range s.appConfigs {
1035 if strings.ToLower(cfg.Network) == network {
1036 return false
1037 }
1038 }
1039 return true
1040}
1041
1042func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +04001043 s.l.Lock()
1044 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +04001045 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +04001046 network = strings.ToLower(network)
1047 if !s.isNetworkUseAllowed(network) {
1048 return fmt.Errorf("network already used: %s", network)
1049 }
gio33059762024-07-05 13:19:07 +04001050 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001051 return err
gio33059762024-07-05 13:19:07 +04001052 } else if ok {
gio11617ac2024-07-15 16:09:04 +04001053 return nil
gioa60f0de2024-07-08 10:49:48 +04001054 }
gio5e49bb62024-07-20 10:43:19 +04001055 networks, err := s.getNetworks(user)
1056 if err != nil {
1057 return err
1058 }
giod8ab4f52024-07-26 16:58:34 +04001059 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +04001060 if !ok {
1061 return fmt.Errorf("network not found: %s\n", network)
1062 }
gio33059762024-07-05 13:19:07 +04001063 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001064 return err
gio33059762024-07-05 13:19:07 +04001065 }
1066 appRepo, err := s.client.GetRepo(appName)
1067 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001068 return err
gio33059762024-07-05 13:19:07 +04001069 }
gio7fbd4ad2024-08-27 10:06:39 +04001070 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
1071 if err != nil {
1072 return err
1073 }
1074 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1075}
1076
1077func (s *DodoAppServer) createDevBranch(appName, fromBranch, toBranch, user string) error {
1078 s.l.Lock()
1079 defer s.l.Unlock()
1080 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1081 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1082 if err != nil {
1083 return err
1084 }
gioc81a8472024-09-24 13:06:19 +02001085 appCfg, err := soft.ReadFile(appRepo, "app.json")
gio7fbd4ad2024-08-27 10:06:39 +04001086 if err != nil {
1087 return err
1088 }
1089 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1090 if err != nil {
1091 return err
1092 }
gioc81a8472024-09-24 13:06:19 +02001093 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
gio7fbd4ad2024-08-27 10:06:39 +04001094}
1095
gio5887caa2024-10-03 15:07:23 +04001096func (s *DodoAppServer) deleteBranch(appName string, branch string) error {
1097 appBranch := fmt.Sprintf("dodo_%s", branch)
1098 hf := installer.NewGitHelmFetcher()
1099 if err := func() error {
1100 repo, err := s.client.GetRepoBranch(appName, appBranch)
1101 if err != nil {
1102 return err
1103 }
1104 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
1105 if err != nil {
1106 return err
1107 }
1108 return m.Remove("app")
1109 }(); err != nil {
1110 return err
1111 }
1112 configRepo, err := s.client.GetRepo(ConfigRepoName)
1113 if err != nil {
1114 return err
1115 }
1116 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
1117 if err != nil {
1118 return err
1119 }
1120 appPath := fmt.Sprintf("%s/%s", appName, branch)
gio829b1b72024-10-05 21:50:56 +04001121 if err := m.Remove(appPath); err != nil {
gio5887caa2024-10-03 15:07:23 +04001122 return err
1123 }
1124 if err := s.client.DeleteRepoBranch(appName, appBranch); err != nil {
1125 return err
1126 }
1127 if branch != "master" {
gioe44c1512024-10-06 14:13:55 +04001128 if err := s.client.DeleteRepoBranch(appName, branch); err != nil {
1129 return err
1130 }
gio5887caa2024-10-03 15:07:23 +04001131 }
gioe44c1512024-10-06 14:13:55 +04001132 return s.st.DeleteBranch(appName, branch)
gio5887caa2024-10-03 15:07:23 +04001133}
1134
1135func (s *DodoAppServer) deleteApp(appName string) error {
1136 configRepo, err := s.client.GetRepo(ConfigRepoName)
1137 if err != nil {
1138 return err
1139 }
1140 branches, err := configRepo.ListDir(fmt.Sprintf("/%s", appName))
1141 if err != nil {
1142 return err
1143 }
1144 for _, b := range branches {
1145 if !b.IsDir() || strings.HasPrefix(b.Name(), "dodo_") {
1146 continue
1147 }
1148 if err := s.deleteBranch(appName, b.Name()); err != nil {
1149 return err
1150 }
1151 }
gioe44c1512024-10-06 14:13:55 +04001152 if err := s.client.DeleteRepo(appName); err != nil {
1153 return err
1154 }
1155 return s.st.DeleteApp(appName)
gio5887caa2024-10-03 15:07:23 +04001156}
1157
gio7fbd4ad2024-08-27 10:06:39 +04001158func (s *DodoAppServer) createAppForBranch(
1159 repo soft.RepoIO,
1160 appName string,
1161 branch string,
1162 user string,
1163 network string,
1164 files map[string][]byte,
1165) error {
1166 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1167 for path, contents := range files {
1168 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1169 return "", err
1170 }
1171 }
1172 return "init", nil
1173 }, soft.WithCommitToBranch(branch))
1174 if err != nil {
1175 return err
1176 }
1177 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001178 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001179 return err
gio33059762024-07-05 13:19:07 +04001180 }
giof15b9da2024-09-19 06:59:16 +04001181 // TODO(gio): get only available ones by owner
1182 clusters, err := s.getClusters()
1183 if err != nil {
1184 return err
1185 }
gio33059762024-07-05 13:19:07 +04001186 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001187 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1188 if err != nil {
1189 return err
1190 }
1191 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001192 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001193 return err
gio33059762024-07-05 13:19:07 +04001194 }
1195 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1196 suffix, err := suffixGen.Generate()
1197 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001198 return err
gio33059762024-07-05 13:19:07 +04001199 }
gio94904702024-07-26 16:58:34 +04001200 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001201 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001202 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001203 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001204 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001205 return err
1206 }
gio7fbd4ad2024-08-27 10:06:39 +04001207 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001208 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001209 return err
gio33059762024-07-05 13:19:07 +04001210 }
giod8ab4f52024-07-26 16:58:34 +04001211 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001212 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001213 return err
gio33059762024-07-05 13:19:07 +04001214 }
1215 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001216 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001217 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001218 return err
gio33059762024-07-05 13:19:07 +04001219 }
gio7fbd4ad2024-08-27 10:06:39 +04001220 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001221 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001222 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001223 if err != nil {
1224 return "", err
1225 }
1226 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001227 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001228 return "", err
1229 }
1230 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001231 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001232 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001233 appPath,
gio9d66f322024-07-06 13:45:10 +04001234 namespace,
1235 map[string]any{
1236 "repoAddr": s.client.GetRepoAddress(appName),
1237 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001238 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001239 "gitRepoPublicKey": s.gitRepoPublicKey,
1240 },
1241 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001242 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001243 installer.WithNoPublish(),
1244 installer.WithNoLock(),
1245 ); err != nil {
1246 return "", err
1247 }
1248 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001249 })
1250 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001251 return err
gio33059762024-07-05 13:19:07 +04001252 }
gio7fbd4ad2024-08-27 10:06:39 +04001253 return s.initAppACLs(m, appPath, appName, branch, user)
1254}
1255
1256func (s *DodoAppServer) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
1257 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001258 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001259 return err
gio33059762024-07-05 13:19:07 +04001260 }
1261 fluxKeys, ok := cfg.Input["fluxKeys"]
1262 if !ok {
gio11617ac2024-07-15 16:09:04 +04001263 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001264 }
1265 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1266 if !ok {
gio11617ac2024-07-15 16:09:04 +04001267 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001268 }
1269 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001270 return err
gio33059762024-07-05 13:19:07 +04001271 } else if ok {
1272 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001273 return err
gio33059762024-07-05 13:19:07 +04001274 }
1275 } else {
1276 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001277 return err
gio33059762024-07-05 13:19:07 +04001278 }
1279 }
gio7fbd4ad2024-08-27 10:06:39 +04001280 if branch != "master" {
1281 return nil
1282 }
gio33059762024-07-05 13:19:07 +04001283 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001284 return err
gio33059762024-07-05 13:19:07 +04001285 }
gio7fbd4ad2024-08-27 10:06:39 +04001286 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001287 return err
gio33059762024-07-05 13:19:07 +04001288 }
gio7fbd4ad2024-08-27 10:06:39 +04001289 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 +04001290 return err
gio33059762024-07-05 13:19:07 +04001291 }
gio2ccb6e32024-08-15 12:01:33 +04001292 if !s.external {
1293 go func() {
1294 users, err := s.client.GetAllUsers()
1295 if err != nil {
1296 fmt.Println(err)
1297 return
1298 }
1299 for _, user := range users {
1300 // TODO(gio): fluxcd should have only read access
1301 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1302 fmt.Println(err)
1303 }
1304 }
1305 }()
1306 }
gio43b0f422024-08-21 10:40:13 +04001307 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1308 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001309 return nil
gio33059762024-07-05 13:19:07 +04001310}
1311
gio81246f02024-07-10 12:02:15 +04001312type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001313 User string `json:"user"`
1314 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001315}
1316
gio7fbd4ad2024-08-27 10:06:39 +04001317func (s *DodoAppServer) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001318 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001319 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1320 http.Error(w, err.Error(), http.StatusBadRequest)
1321 return
1322 }
gio7fbd4ad2024-08-27 10:06:39 +04001323 if req.User == "" {
1324 http.Error(w, "invalid user", http.StatusBadRequest)
1325 return
1326 }
1327 if req.PublicKey == "" {
1328 http.Error(w, "invalid public key", http.StatusBadRequest)
1329 return
1330 }
1331 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001332 http.Error(w, err.Error(), http.StatusInternalServerError)
1333 return
1334 }
1335}
1336
gio94904702024-07-26 16:58:34 +04001337type dodoAppRendered struct {
1338 App struct {
1339 Ingress struct {
1340 Network string `json:"network"`
1341 Subdomain string `json:"subdomain"`
1342 } `json:"ingress"`
1343 } `json:"app"`
1344 Input struct {
1345 AppId string `json:"appId"`
1346 } `json:"input"`
1347}
1348
gio7fbd4ad2024-08-27 10:06:39 +04001349// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio43b0f422024-08-21 10:40:13 +04001350func (s *DodoAppServer) updateDodoApp(
1351 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001352 name string,
1353 branch string,
1354 namespace string,
gio43b0f422024-08-21 10:40:13 +04001355 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001356 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001357 owner string,
gio43b0f422024-08-21 10:40:13 +04001358) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001359 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001360 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001361 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001362 }
giof8843412024-05-22 16:38:05 +04001363 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001364 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001365 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001366 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001367 }
gioc81a8472024-09-24 13:06:19 +02001368 appCfg, err := soft.ReadFile(repo, "app.json")
gio0eaf2712024-04-14 13:08:46 +04001369 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001370 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001371 }
1372 app, err := installer.NewDodoApp(appCfg)
1373 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001374 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001375 }
giof8843412024-05-22 16:38:05 +04001376 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001377 var ret installer.ReleaseResources
1378 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1379 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001380 app,
1381 "app",
1382 "/.dodo/app",
1383 namespace,
1384 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001385 "repoAddr": repo.FullAddress(),
1386 "repoPublicAddr": s.repoPublicAddr,
1387 "managerAddr": fmt.Sprintf("http://%s", s.self),
1388 "appId": name,
1389 "branch": branch,
1390 "sshPrivateKey": s.sshKey,
1391 "username": owner,
gio94904702024-07-26 16:58:34 +04001392 },
1393 installer.WithNoPull(),
1394 installer.WithNoPublish(),
1395 installer.WithConfig(&s.env),
1396 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001397 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001398 installer.WithLocalChartGenerator(lg),
1399 installer.WithNoLock(),
1400 )
1401 if err != nil {
1402 return "", err
1403 }
1404 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001405 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001406 return "", nil
1407 }
1408 if _, err := m.Install(
1409 appStatus,
1410 "status",
1411 "/.dodo/status",
1412 s.namespace,
1413 map[string]any{
1414 "appName": rendered.Input.AppId,
1415 "network": rendered.App.Ingress.Network,
1416 "appSubdomain": rendered.App.Ingress.Subdomain,
1417 },
1418 installer.WithNoPull(),
1419 installer.WithNoPublish(),
1420 installer.WithConfig(&s.env),
1421 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001422 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001423 installer.WithLocalChartGenerator(lg),
1424 installer.WithNoLock(),
1425 ); err != nil {
1426 return "", err
1427 }
1428 return "install app", nil
1429 },
gio7fbd4ad2024-08-27 10:06:39 +04001430 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001431 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001432 ); err != nil {
1433 return installer.ReleaseResources{}, err
1434 }
gio43b0f422024-08-21 10:40:13 +04001435 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1436 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001437 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001438}
gio33059762024-07-05 13:19:07 +04001439
gio7fbd4ad2024-08-27 10:06:39 +04001440func (s *DodoAppServer) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001441 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001442 appTmpl, err := s.appTmpls.Find(appType)
1443 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001444 return nil, err
gio33059762024-07-05 13:19:07 +04001445 }
gioc81a8472024-09-24 13:06:19 +02001446 return appTmpl.Render(fmt.Sprintf("%s/stat/schemas/dodo_app.jsonschema", s.selfPublic), network, subdomain)
gio33059762024-07-05 13:19:07 +04001447}
gio81246f02024-07-10 12:02:15 +04001448
1449func generatePassword() string {
1450 return "foo"
1451}
giocb34ad22024-07-11 08:01:13 +04001452
gio11617ac2024-07-15 16:09:04 +04001453func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001454 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001455 resp, err := http.Get(addr)
1456 if err != nil {
1457 return nil, err
1458 }
gio23bdc1b2024-07-11 16:07:47 +04001459 networks := []installer.Network{}
1460 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001461 return nil, err
1462 }
gio11617ac2024-07-15 16:09:04 +04001463 return s.nf.Filter(user, networks)
1464}
1465
giof15b9da2024-09-19 06:59:16 +04001466func (s *DodoAppServer) getClusters() ([]installer.Cluster, error) {
1467 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1468 resp, err := http.Get(addr)
1469 if err != nil {
1470 return nil, err
1471 }
1472 clusters := []installer.Cluster{}
1473 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1474 return nil, err
1475 }
1476 fmt.Printf("CLUSTERS %+v\n", clusters)
1477 return clusters, nil
1478}
1479
gio8fae3af2024-07-25 13:43:31 +04001480type publicNetworkData struct {
1481 Name string `json:"name"`
1482 Domain string `json:"domain"`
1483}
1484
1485type publicData struct {
1486 Networks []publicNetworkData `json:"networks"`
1487 Types []string `json:"types"`
1488}
1489
1490func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001491 w.Header().Set("Access-Control-Allow-Origin", "*")
1492 s.l.Lock()
1493 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001494 networks, err := s.getNetworks("")
1495 if err != nil {
1496 http.Error(w, err.Error(), http.StatusInternalServerError)
1497 return
1498 }
1499 var ret publicData
1500 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001501 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1502 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1503 }
gio8fae3af2024-07-25 13:43:31 +04001504 }
1505 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001506 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001507 }
gio8fae3af2024-07-25 13:43:31 +04001508 if err := json.NewEncoder(w).Encode(ret); err != nil {
1509 http.Error(w, err.Error(), http.StatusInternalServerError)
1510 return
1511 }
1512}
1513
gio7fbd4ad2024-08-27 10:06:39 +04001514func (s *DodoAppServer) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001515 if err != nil {
1516 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001517 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001518 fmt.Printf("Error: %s\n", err.Error())
1519 return err
1520 }
1521 return err
1522 }
1523 var resB bytes.Buffer
1524 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001525 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001526 fmt.Printf("Error: %s\n", err.Error())
1527 return err
1528 }
1529 return err
1530 }
gio7fbd4ad2024-08-27 10:06:39 +04001531 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001532 fmt.Printf("Error: %s\n", err.Error())
1533 return err
1534 }
1535 return nil
1536}
1537
gio11617ac2024-07-15 16:09:04 +04001538func pickNetwork(networks []installer.Network, network string) []installer.Network {
1539 for _, n := range networks {
1540 if n.Name == network {
1541 return []installer.Network{n}
1542 }
1543 }
1544 return []installer.Network{}
1545}
1546
1547type NetworkFilter interface {
1548 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1549}
1550
1551type noNetworkFilter struct{}
1552
1553func NewNoNetworkFilter() NetworkFilter {
1554 return noNetworkFilter{}
1555}
1556
gio8fae3af2024-07-25 13:43:31 +04001557func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001558 return networks, nil
1559}
1560
1561type filterByOwner struct {
1562 st Store
1563}
1564
1565func NewNetworkFilterByOwner(st Store) NetworkFilter {
1566 return &filterByOwner{st}
1567}
1568
1569func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001570 if user == "" {
1571 return networks, nil
1572 }
gio11617ac2024-07-15 16:09:04 +04001573 network, err := f.st.GetUserNetwork(user)
1574 if err != nil {
1575 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001576 }
1577 ret := []installer.Network{}
1578 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001579 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001580 ret = append(ret, n)
1581 }
1582 }
giocb34ad22024-07-11 08:01:13 +04001583 return ret, nil
1584}
gio11617ac2024-07-15 16:09:04 +04001585
1586type allowListFilter struct {
1587 allowed []string
1588}
1589
1590func NewAllowListFilter(allowed []string) NetworkFilter {
1591 return &allowListFilter{allowed}
1592}
1593
gio8fae3af2024-07-25 13:43:31 +04001594func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001595 ret := []installer.Network{}
1596 for _, n := range networks {
1597 if slices.Contains(f.allowed, n.Name) {
1598 ret = append(ret, n)
1599 }
1600 }
1601 return ret, nil
1602}
1603
1604type combinedNetworkFilter struct {
1605 filters []NetworkFilter
1606}
1607
1608func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1609 return &combinedNetworkFilter{filters}
1610}
1611
gio8fae3af2024-07-25 13:43:31 +04001612func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001613 ret := networks
1614 var err error
1615 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001616 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001617 if err != nil {
1618 return nil, err
1619 }
1620 }
1621 return ret, nil
1622}
giocafd4e62024-07-31 10:53:40 +04001623
1624type user struct {
1625 Username string `json:"username"`
1626 Email string `json:"email"`
1627 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1628}
1629
1630func (s *DodoAppServer) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
1631 go s.syncUsers()
1632}
1633
1634func (s *DodoAppServer) syncUsers() {
1635 if s.external {
1636 panic("MUST NOT REACH!")
1637 }
1638 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1639 if err != nil {
1640 return
1641 }
1642 users := []user{}
1643 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1644 fmt.Println(err)
1645 return
1646 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001647 validUsernames := make(map[string]user)
1648 for _, u := range users {
1649 validUsernames[u.Username] = u
1650 }
1651 allClientUsers, err := s.client.GetAllUsers()
1652 if err != nil {
1653 fmt.Println(err)
1654 return
1655 }
1656 keyToUser := make(map[string]string)
1657 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001658 if clientUser == "admin" || clientUser == "fluxcd" {
1659 continue
1660 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001661 userData, ok := validUsernames[clientUser]
1662 if !ok {
1663 if err := s.client.RemoveUser(clientUser); err != nil {
1664 fmt.Println(err)
1665 return
1666 }
1667 } else {
1668 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1669 if err != nil {
1670 fmt.Println(err)
1671 return
1672 }
1673 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001674 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001675 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001676 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001677 })
1678 if !keyOk {
1679 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1680 fmt.Println(err)
1681 }
1682 } else {
1683 keyToUser[cleanKey] = clientUser
1684 }
1685 }
1686 }
1687 }
giocafd4e62024-07-31 10:53:40 +04001688 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001689 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1690 fmt.Println(err)
1691 return
1692 }
giocafd4e62024-07-31 10:53:40 +04001693 if len(u.SSHPublicKeys) == 0 {
1694 continue
1695 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001696 ok, err := s.client.UserExists(u.Username)
1697 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001698 fmt.Println(err)
1699 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001700 }
1701 if !ok {
1702 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1703 fmt.Println(err)
1704 return
1705 }
1706 } else {
1707 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001708 cleanKey := soft.CleanKey(key)
1709 if user, ok := keyToUser[cleanKey]; ok {
1710 if u.Username != user {
1711 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1712 }
1713 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001714 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001715 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001716 fmt.Println(err)
1717 return
giocafd4e62024-07-31 10:53:40 +04001718 }
1719 }
1720 }
1721 }
1722 repos, err := s.client.GetAllRepos()
1723 if err != nil {
1724 return
1725 }
1726 for _, r := range repos {
1727 if r == ConfigRepoName {
1728 continue
1729 }
1730 for _, u := range users {
1731 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1732 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001733 continue
giocafd4e62024-07-31 10:53:40 +04001734 }
1735 }
1736 }
1737}
giob4a3a192024-08-19 09:55:47 +04001738
1739func extractResourceData(resources []installer.Resource) (resourceData, error) {
1740 var ret resourceData
1741 for _, r := range resources {
1742 t, ok := r.Annotations["dodo.cloud/resource-type"]
1743 if !ok {
1744 continue
1745 }
1746 switch t {
1747 case "volume":
1748 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1749 if !ok {
1750 return resourceData{}, fmt.Errorf("no name")
1751 }
1752 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1753 if !ok {
1754 return resourceData{}, fmt.Errorf("no size")
1755 }
1756 ret.Volume = append(ret.Volume, volume{name, size})
1757 case "postgresql":
1758 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1759 if !ok {
1760 return resourceData{}, fmt.Errorf("no name")
1761 }
1762 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1763 if !ok {
1764 return resourceData{}, fmt.Errorf("no version")
1765 }
1766 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1767 if !ok {
1768 return resourceData{}, fmt.Errorf("no volume")
1769 }
1770 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1771 case "ingress":
1772 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1773 if !ok {
1774 return resourceData{}, fmt.Errorf("no host")
1775 }
1776 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001777 case "virtual-machine":
1778 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1779 if !ok {
1780 return resourceData{}, fmt.Errorf("no name")
1781 }
1782 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1783 if !ok {
1784 return resourceData{}, fmt.Errorf("no user")
1785 }
1786 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1787 if !ok {
1788 return resourceData{}, fmt.Errorf("no cpu cores")
1789 }
1790 cpuCores, err := strconv.Atoi(cpuCoresS)
1791 if err != nil {
1792 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1793 }
1794 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1795 if !ok {
1796 return resourceData{}, fmt.Errorf("no memory")
1797 }
1798 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001799 default:
1800 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1801 }
1802 }
1803 return ret, nil
1804}
gio7fbd4ad2024-08-27 10:06:39 +04001805
1806func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
gioc81a8472024-09-24 13:06:19 +02001807 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
1808 "app.cue": from,
1809 })
gio7fbd4ad2024-08-27 10:06:39 +04001810 if err != nil {
1811 return "", nil, err
1812 }
1813 if err := cfg.Err(); err != nil {
1814 return "", nil, err
1815 }
1816 if err := cfg.Validate(); err != nil {
1817 return "", nil, err
1818 }
1819 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1820 if err := subdomain.Err(); err != nil {
1821 return "", nil, err
1822 }
1823 subdomainStr, err := subdomain.String()
1824 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1825 if err := network.Err(); err != nil {
1826 return "", nil, err
1827 }
1828 networkStr, err := network.String()
1829 if err != nil {
1830 return "", nil, err
1831 }
1832 newCfg := map[string]any{}
1833 if err := cfg.Decode(&newCfg); err != nil {
1834 return "", nil, err
1835 }
1836 app, ok := newCfg["app"].(map[string]any)
1837 if !ok {
1838 return "", nil, fmt.Errorf("not a map")
1839 }
1840 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1841 app["dev"] = map[string]any{
1842 "enabled": true,
1843 "username": username,
1844 }
1845 buf, err := json.MarshalIndent(newCfg, "", "\t")
1846 if err != nil {
1847 return "", nil, err
1848 }
1849 return networkStr, buf, nil
1850}