blob: 858afa8906e86ee68636d8329029ae6cdff53570 [file] [log] [blame]
gio59946282024-10-07 12:55:51 +04001package dodoapp
gio0eaf2712024-04-14 13:08:46 +04002
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"
giof078f462024-10-14 09:07:33 +040015 "sort"
gio7fbd4ad2024-08-27 10:06:39 +040016 "strconv"
gio0eaf2712024-04-14 13:08:46 +040017 "strings"
gio9d66f322024-07-06 13:45:10 +040018 "sync"
giocafd4e62024-07-31 10:53:40 +040019 "time"
gio0eaf2712024-04-14 13:08:46 +040020
Davit Tabidzea5ea5092024-08-01 15:28:09 +040021 "golang.org/x/crypto/bcrypt"
22 "golang.org/x/exp/rand"
23
gio0eaf2712024-04-14 13:08:46 +040024 "github.com/giolekva/pcloud/core/installer"
gio59946282024-10-07 12:55:51 +040025 "github.com/giolekva/pcloud/core/installer/server"
gio0eaf2712024-04-14 13:08:46 +040026 "github.com/giolekva/pcloud/core/installer/soft"
gio43b0f422024-08-21 10:40:13 +040027 "github.com/giolekva/pcloud/core/installer/tasks"
gio33059762024-07-05 13:19:07 +040028
gio7fbd4ad2024-08-27 10:06:39 +040029 "cuelang.org/go/cue"
gio33059762024-07-05 13:19:07 +040030 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040031 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040032)
33
gio59946282024-10-07 12:55:51 +040034//go:embed templates/*
35var templates embed.FS
gio23bdc1b2024-07-11 16:07:47 +040036
gio59946282024-10-07 12:55:51 +040037//go:embed all:app-templates
gio5e49bb62024-07-20 10:43:19 +040038var appTmplsFS embed.FS
39
gio59946282024-10-07 12:55:51 +040040//go:embed static/*
41var staticAssets embed.FS
42
43//go:embed static/schemas/app.schema.json
gioc81a8472024-09-24 13:06:19 +020044var dodoAppJsonSchema []byte
45
gio9d66f322024-07-06 13:45:10 +040046const (
gioa60f0de2024-07-08 10:49:48 +040047 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040048 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040049 loginPath = "/login"
50 logoutPath = "/logout"
gio59946282024-10-07 12:55:51 +040051 staticPath = "/static/"
gioc81a8472024-09-24 13:06:19 +020052 schemasPath = "/schemas/"
gio8fae3af2024-07-25 13:43:31 +040053 apiPublicData = "/api/public-data"
54 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040055 sessionCookie = "dodo-app-session"
56 userCtx = "user"
giob4a3a192024-08-19 09:55:47 +040057 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040058)
59
gio59946282024-10-07 12:55:51 +040060type tmplts struct {
giob4a3a192024-08-19 09:55:47 +040061 index *template.Template
62 appStatus *template.Template
63 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040064 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040065}
66
gio59946282024-10-07 12:55:51 +040067func parseTemplates(fs embed.FS) (tmplts, error) {
68 base, err := template.ParseFS(fs, "templates/base.html")
gio23bdc1b2024-07-11 16:07:47 +040069 if err != nil {
gio59946282024-10-07 12:55:51 +040070 return tmplts{}, err
gio23bdc1b2024-07-11 16:07:47 +040071 }
gio5e49bb62024-07-20 10:43:19 +040072 parse := func(path string) (*template.Template, error) {
73 if b, err := base.Clone(); err != nil {
74 return nil, err
75 } else {
76 return b.ParseFS(fs, path)
77 }
78 }
gio59946282024-10-07 12:55:51 +040079 index, err := parse("templates/index.html")
gio5e49bb62024-07-20 10:43:19 +040080 if err != nil {
gio59946282024-10-07 12:55:51 +040081 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040082 }
gio59946282024-10-07 12:55:51 +040083 appStatus, err := parse("templates/app_status.html")
gio5e49bb62024-07-20 10:43:19 +040084 if err != nil {
gio59946282024-10-07 12:55:51 +040085 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040086 }
gio59946282024-10-07 12:55:51 +040087 commitStatus, err := parse("templates/commit_status.html")
giob4a3a192024-08-19 09:55:47 +040088 if err != nil {
gio59946282024-10-07 12:55:51 +040089 return tmplts{}, err
giob4a3a192024-08-19 09:55:47 +040090 }
gio59946282024-10-07 12:55:51 +040091 logs, err := parse("templates/logs.html")
gio183e8342024-08-20 06:01:24 +040092 if err != nil {
gio59946282024-10-07 12:55:51 +040093 return tmplts{}, err
gio183e8342024-08-20 06:01:24 +040094 }
gio59946282024-10-07 12:55:51 +040095 return tmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040096}
97
gio59946282024-10-07 12:55:51 +040098type Server struct {
giocb34ad22024-07-11 08:01:13 +040099 l sync.Locker
100 st Store
gio11617ac2024-07-15 16:09:04 +0400101 nf NetworkFilter
102 ug UserGetter
giocb34ad22024-07-11 08:01:13 +0400103 port int
104 apiPort int
105 self string
gioc81a8472024-09-24 13:06:19 +0200106 selfPublic string
gio11617ac2024-07-15 16:09:04 +0400107 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +0400108 sshKey string
109 gitRepoPublicKey string
110 client soft.Client
111 namespace string
112 envAppManagerAddr string
113 env installer.EnvConfig
114 nsc installer.NamespaceCreator
115 jc installer.JobCreator
gio864b4332024-09-05 13:56:47 +0400116 vpnKeyGen installer.VPNAPIClient
giof6ad2982024-08-23 17:42:49 +0400117 cnc installer.ClusterNetworkConfigurator
giocb34ad22024-07-11 08:01:13 +0400118 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400119 appConfigs map[string]appConfig
gio59946282024-10-07 12:55:51 +0400120 tmplts tmplts
gio5e49bb62024-07-20 10:43:19 +0400121 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400122 external bool
123 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400124 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400125 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400126}
127
128type appConfig struct {
129 Namespace string `json:"namespace"`
130 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400131}
132
gio33059762024-07-05 13:19:07 +0400133// TODO(gio): Initialize appNs on startup
gio59946282024-10-07 12:55:51 +0400134func NewServer(
gioa60f0de2024-07-08 10:49:48 +0400135 st Store,
gio11617ac2024-07-15 16:09:04 +0400136 nf NetworkFilter,
137 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400138 port int,
gioa60f0de2024-07-08 10:49:48 +0400139 apiPort int,
gio33059762024-07-05 13:19:07 +0400140 self string,
gioc81a8472024-09-24 13:06:19 +0200141 selfPublic string,
gio11617ac2024-07-15 16:09:04 +0400142 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400143 sshKey string,
gio33059762024-07-05 13:19:07 +0400144 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400145 client soft.Client,
146 namespace string,
giocb34ad22024-07-11 08:01:13 +0400147 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400148 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400149 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400150 vpnKeyGen installer.VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +0400151 cnc installer.ClusterNetworkConfigurator,
gio0eaf2712024-04-14 13:08:46 +0400152 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400153 external bool,
154 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400155 reconciler tasks.Reconciler,
gio59946282024-10-07 12:55:51 +0400156) (*Server, error) {
157 tmplts, err := parseTemplates(templates)
gio23bdc1b2024-07-11 16:07:47 +0400158 if err != nil {
159 return nil, err
160 }
gio5e4d1a72024-10-09 15:25:29 +0400161 apps, err := fs.Sub(appTmplsFS, "app-templates")
gio5e49bb62024-07-20 10:43:19 +0400162 if err != nil {
163 return nil, err
164 }
165 appTmpls, err := NewAppTmplStoreFS(apps)
166 if err != nil {
167 return nil, err
168 }
gio59946282024-10-07 12:55:51 +0400169 s := &Server{
gio9d66f322024-07-06 13:45:10 +0400170 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400171 st,
gio11617ac2024-07-15 16:09:04 +0400172 nf,
173 ug,
gio0eaf2712024-04-14 13:08:46 +0400174 port,
gioa60f0de2024-07-08 10:49:48 +0400175 apiPort,
gio33059762024-07-05 13:19:07 +0400176 self,
gioc81a8472024-09-24 13:06:19 +0200177 selfPublic,
gio11617ac2024-07-15 16:09:04 +0400178 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400179 sshKey,
gio33059762024-07-05 13:19:07 +0400180 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400181 client,
182 namespace,
giocb34ad22024-07-11 08:01:13 +0400183 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400184 env,
gio33059762024-07-05 13:19:07 +0400185 nsc,
giof8843412024-05-22 16:38:05 +0400186 jc,
gio36b23b32024-08-25 12:20:54 +0400187 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +0400188 cnc,
gio266c04f2024-07-03 14:18:45 +0400189 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400190 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400191 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400192 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400193 external,
194 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400195 reconciler,
gio183e8342024-08-20 06:01:24 +0400196 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400197 }
gioa60f0de2024-07-08 10:49:48 +0400198 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400199 if err != nil {
200 return nil, err
201 }
giod8ab4f52024-07-26 16:58:34 +0400202 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400203 if err == nil {
204 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400205 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400206 return nil, err
207 }
208 } else if !errors.Is(err, fs.ErrNotExist) {
209 return nil, err
210 }
211 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400212}
213
gio59946282024-10-07 12:55:51 +0400214func (s *Server) getAppConfig(app, branch string) appConfig {
gio7fbd4ad2024-08-27 10:06:39 +0400215 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
216}
217
gio59946282024-10-07 12:55:51 +0400218func (s *Server) setAppConfig(app, branch string, cfg appConfig) {
gio7fbd4ad2024-08-27 10:06:39 +0400219 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
220}
221
gio59946282024-10-07 12:55:51 +0400222func (s *Server) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400223 // if err := s.client.DisableKeyless(); err != nil {
224 // return err
225 // }
226 // if err := s.client.DisableAnonAccess(); err != nil {
227 // return err
228 // }
gioa60f0de2024-07-08 10:49:48 +0400229 e := make(chan error)
230 go func() {
231 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400232 r.Use(s.mwAuth)
gioc81a8472024-09-24 13:06:19 +0200233 r.HandleFunc(schemasPath+"app.schema.json", s.handleSchema).Methods(http.MethodGet)
gio59946282024-10-07 12:55:51 +0400234 r.PathPrefix(staticPath).Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
gio81246f02024-07-10 12:02:15 +0400235 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400236 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
237 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400238 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
239 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400240 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400241 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400242 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
243 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400244 r.HandleFunc("/{app-name}/branch/{branch}/delete", s.handleBranchDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400245 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400246 r.HandleFunc("/{app-name}/delete", s.handleAppDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400247 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400248 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400249 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
250 }()
251 go func() {
252 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400253 r.HandleFunc("/update", s.handleAPIUpdate)
254 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400255 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400256 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400257 if !s.external {
258 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
259 }
gioa60f0de2024-07-08 10:49:48 +0400260 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
261 }()
giocafd4e62024-07-31 10:53:40 +0400262 if !s.external {
263 go func() {
264 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400265 for {
266 delay := time.Duration(rand.Intn(60)+60) * time.Second
267 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400268 s.syncUsers()
269 }
270 }()
271 }
gioa60f0de2024-07-08 10:49:48 +0400272 return <-e
273}
274
gio11617ac2024-07-15 16:09:04 +0400275type UserGetter interface {
276 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400277 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400278}
279
280type externalUserGetter struct {
281 sc *securecookie.SecureCookie
282}
283
284func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400285 return &externalUserGetter{securecookie.New(
286 securecookie.GenerateRandomKey(64),
287 securecookie.GenerateRandomKey(32),
288 )}
gio11617ac2024-07-15 16:09:04 +0400289}
290
291func (ug *externalUserGetter) Get(r *http.Request) string {
292 cookie, err := r.Cookie(sessionCookie)
293 if err != nil {
294 return ""
295 }
296 var user string
297 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
298 return ""
299 }
300 return user
301}
302
gio8fae3af2024-07-25 13:43:31 +0400303func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
304 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
305 cookie := &http.Cookie{
306 Name: sessionCookie,
307 Value: encoded,
308 Path: "/",
309 Secure: true,
310 HttpOnly: true,
311 }
312 http.SetCookie(w, cookie)
313 return nil
314 } else {
315 return err
316 }
317}
318
gio11617ac2024-07-15 16:09:04 +0400319type internalUserGetter struct{}
320
321func NewInternalUserGetter() UserGetter {
322 return internalUserGetter{}
323}
324
325func (ug internalUserGetter) Get(r *http.Request) string {
giodd213152024-09-27 11:26:59 +0200326 return r.Header.Get("X-Forwarded-User")
gio11617ac2024-07-15 16:09:04 +0400327}
328
gio8fae3af2024-07-25 13:43:31 +0400329func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
330 return nil
331}
332
gio59946282024-10-07 12:55:51 +0400333func (s *Server) mwAuth(next http.Handler) http.Handler {
gio81246f02024-07-10 12:02:15 +0400334 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400335 if strings.HasSuffix(r.URL.Path, loginPath) ||
336 strings.HasPrefix(r.URL.Path, logoutPath) ||
337 strings.HasPrefix(r.URL.Path, staticPath) ||
gioc81a8472024-09-24 13:06:19 +0200338 strings.HasPrefix(r.URL.Path, schemasPath) ||
gio8fae3af2024-07-25 13:43:31 +0400339 strings.HasPrefix(r.URL.Path, apiPublicData) ||
340 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400341 next.ServeHTTP(w, r)
342 return
343 }
gio11617ac2024-07-15 16:09:04 +0400344 user := s.ug.Get(r)
345 if user == "" {
gio81246f02024-07-10 12:02:15 +0400346 vars := mux.Vars(r)
347 appName, ok := vars["app-name"]
348 if !ok || appName == "" {
349 http.Error(w, "missing app-name", http.StatusBadRequest)
350 return
351 }
352 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
353 return
354 }
gio81246f02024-07-10 12:02:15 +0400355 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
356 })
357}
358
gio59946282024-10-07 12:55:51 +0400359func (s *Server) handleSchema(w http.ResponseWriter, r *http.Request) {
gioc81a8472024-09-24 13:06:19 +0200360 w.Header().Set("Content-Type", "application/schema+json")
361 w.Write(dodoAppJsonSchema)
362}
363
gio59946282024-10-07 12:55:51 +0400364func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400365 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400366 http.SetCookie(w, &http.Cookie{
367 Name: sessionCookie,
368 Value: "",
369 Path: "/",
370 HttpOnly: true,
371 Secure: true,
372 })
373 http.Redirect(w, r, "/", http.StatusSeeOther)
374}
375
gio59946282024-10-07 12:55:51 +0400376func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400377 vars := mux.Vars(r)
378 appName, ok := vars["app-name"]
379 if !ok || appName == "" {
380 http.Error(w, "missing app-name", http.StatusBadRequest)
381 return
382 }
383 fmt.Fprint(w, `
384<!DOCTYPE html>
385<html lang='en'>
386 <head>
387 <title>dodo: app - login</title>
388 <meta charset='utf-8'>
389 </head>
390 <body>
391 <form action="" method="POST">
392 <input type="password" placeholder="Password" name="password" required />
393 <button type="submit">Login</button>
394 </form>
395 </body>
396</html>
397`)
398}
399
gio59946282024-10-07 12:55:51 +0400400func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400401 vars := mux.Vars(r)
402 appName, ok := vars["app-name"]
403 if !ok || appName == "" {
404 http.Error(w, "missing app-name", http.StatusBadRequest)
405 return
406 }
407 password := r.FormValue("password")
408 if password == "" {
409 http.Error(w, "missing password", http.StatusBadRequest)
410 return
411 }
412 user, err := s.st.GetAppOwner(appName)
413 if err != nil {
414 http.Error(w, err.Error(), http.StatusInternalServerError)
415 return
416 }
417 hashed, err := s.st.GetUserPassword(user)
418 if err != nil {
419 http.Error(w, err.Error(), http.StatusInternalServerError)
420 return
421 }
422 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
423 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
424 return
425 }
gio8fae3af2024-07-25 13:43:31 +0400426 if err := s.ug.Encode(w, user); err != nil {
427 http.Error(w, err.Error(), http.StatusInternalServerError)
428 return
gio81246f02024-07-10 12:02:15 +0400429 }
430 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
431}
432
giob4a3a192024-08-19 09:55:47 +0400433type navItem struct {
434 Name string
435 Address string
436}
437
gio23bdc1b2024-07-11 16:07:47 +0400438type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400439 Navigation []navItem
440 Apps []string
441 Networks []installer.Network
442 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400443}
444
gio59946282024-10-07 12:55:51 +0400445func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400446 user := r.Context().Value(userCtx)
447 if user == nil {
448 http.Error(w, "unauthorized", http.StatusUnauthorized)
449 return
450 }
451 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400452 if err != nil {
453 http.Error(w, err.Error(), http.StatusInternalServerError)
454 return
455 }
gio11617ac2024-07-15 16:09:04 +0400456 networks, err := s.getNetworks(user.(string))
457 if err != nil {
458 http.Error(w, err.Error(), http.StatusInternalServerError)
459 return
460 }
giob54db242024-07-30 18:49:33 +0400461 var types []string
462 for _, t := range s.appTmpls.Types() {
463 types = append(types, strings.Replace(t, "-", ":", 1))
464 }
giob4a3a192024-08-19 09:55:47 +0400465 n := []navItem{navItem{"Home", "/"}}
466 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400467 if err := s.tmplts.index.Execute(w, data); err != nil {
468 http.Error(w, err.Error(), http.StatusInternalServerError)
469 return
gioa60f0de2024-07-08 10:49:48 +0400470 }
471}
472
gio5e49bb62024-07-20 10:43:19 +0400473type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400474 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400475 Name string
gio5887caa2024-10-03 15:07:23 +0400476 Branch string
gio5e49bb62024-07-20 10:43:19 +0400477 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400478 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400479 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400480 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400481}
482
gio59946282024-10-07 12:55:51 +0400483func (s *Server) handleAppStatus(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400484 vars := mux.Vars(r)
485 appName, ok := vars["app-name"]
486 if !ok || appName == "" {
487 http.Error(w, "missing app-name", http.StatusBadRequest)
488 return
489 }
gio7fbd4ad2024-08-27 10:06:39 +0400490 branch, ok := vars["branch"]
491 if !ok || branch == "" {
492 branch = "master"
493 }
gio94904702024-07-26 16:58:34 +0400494 u := r.Context().Value(userCtx)
495 if u == nil {
496 http.Error(w, "unauthorized", http.StatusUnauthorized)
497 return
498 }
499 user, ok := u.(string)
500 if !ok {
501 http.Error(w, "could not get user", http.StatusInternalServerError)
502 return
503 }
504 owner, err := s.st.GetAppOwner(appName)
505 if err != nil {
506 http.Error(w, err.Error(), http.StatusInternalServerError)
507 return
508 }
509 if owner != user {
510 http.Error(w, "unauthorized", http.StatusUnauthorized)
511 return
512 }
gio7fbd4ad2024-08-27 10:06:39 +0400513 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400514 if err != nil {
515 http.Error(w, err.Error(), http.StatusInternalServerError)
516 return
517 }
gio183e8342024-08-20 06:01:24 +0400518 var lastCommitResources resourceData
519 if len(commits) > 0 {
520 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
521 if err != nil {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
524 }
525 r, err := extractResourceData(lastCommit.Resources.Helm)
526 if err != nil {
527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
529 }
530 lastCommitResources = r
531 }
gio7fbd4ad2024-08-27 10:06:39 +0400532 branches, err := s.st.GetBranches(appName)
533 if err != nil {
534 http.Error(w, err.Error(), http.StatusInternalServerError)
535 return
536 }
gio5e49bb62024-07-20 10:43:19 +0400537 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400538 Navigation: []navItem{
539 navItem{"Home", "/"},
540 navItem{appName, "/" + appName},
541 },
gio5e49bb62024-07-20 10:43:19 +0400542 Name: appName,
gio5887caa2024-10-03 15:07:23 +0400543 Branch: branch,
gio5e49bb62024-07-20 10:43:19 +0400544 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
545 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400546 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400547 Branches: branches,
548 }
549 if branch != "master" {
550 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400551 }
552 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
553 http.Error(w, err.Error(), http.StatusInternalServerError)
554 return
gioa60f0de2024-07-08 10:49:48 +0400555 }
gio0eaf2712024-04-14 13:08:46 +0400556}
557
giocfb228c2024-09-06 15:44:31 +0400558type appEnv struct {
559 Profile string `json:"envProfile"`
560}
561
gio59946282024-10-07 12:55:51 +0400562func (s *Server) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
giocfb228c2024-09-06 15:44:31 +0400563 vars := mux.Vars(r)
564 appName, ok := vars["app-name"]
565 if !ok || appName == "" {
566 http.Error(w, "missing app-name", http.StatusBadRequest)
567 return
568 }
569 branch, ok := vars["branch"]
570 if !ok || branch == "" {
571 branch = "master"
572 }
573 info, err := s.st.GetLastCommitInfo(appName, branch)
574 if err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
578 var e appEnv
579 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
580 http.Error(w, err.Error(), http.StatusInternalServerError)
581 return
582 }
583 fmt.Fprintln(w, e.Profile)
584}
585
giob4a3a192024-08-19 09:55:47 +0400586type volume struct {
587 Name string
588 Size string
589}
590
591type postgresql struct {
592 Name string
593 Version string
594 Volume string
595}
596
597type ingress struct {
giof078f462024-10-14 09:07:33 +0400598 Name string
giob4a3a192024-08-19 09:55:47 +0400599 Host string
giof078f462024-10-14 09:07:33 +0400600 Home string
giob4a3a192024-08-19 09:55:47 +0400601}
602
gio7fbd4ad2024-08-27 10:06:39 +0400603type vm struct {
604 Name string
605 User string
606 CPUCores int
607 Memory string
608}
609
giob4a3a192024-08-19 09:55:47 +0400610type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400611 Volume []volume
612 PostgreSQL []postgresql
613 Ingress []ingress
614 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400615}
616
617type commitStatusData struct {
618 Navigation []navItem
619 AppName string
620 Commit Commit
621 Resources resourceData
622}
623
gio59946282024-10-07 12:55:51 +0400624func (s *Server) handleAppCommit(w http.ResponseWriter, r *http.Request) {
giob4a3a192024-08-19 09:55:47 +0400625 vars := mux.Vars(r)
626 appName, ok := vars["app-name"]
627 if !ok || appName == "" {
628 http.Error(w, "missing app-name", http.StatusBadRequest)
629 return
630 }
631 hash, ok := vars["hash"]
632 if !ok || appName == "" {
633 http.Error(w, "missing app-name", http.StatusBadRequest)
634 return
635 }
636 u := r.Context().Value(userCtx)
637 if u == nil {
638 http.Error(w, "unauthorized", http.StatusUnauthorized)
639 return
640 }
641 user, ok := u.(string)
642 if !ok {
643 http.Error(w, "could not get user", http.StatusInternalServerError)
644 return
645 }
646 owner, err := s.st.GetAppOwner(appName)
647 if err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
651 if owner != user {
652 http.Error(w, "unauthorized", http.StatusUnauthorized)
653 return
654 }
655 commit, err := s.st.GetCommit(hash)
656 if err != nil {
657 // TODO(gio): not-found ?
658 http.Error(w, err.Error(), http.StatusInternalServerError)
659 return
660 }
661 var res strings.Builder
662 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
663 http.Error(w, err.Error(), http.StatusInternalServerError)
664 return
665 }
666 resData, err := extractResourceData(commit.Resources.Helm)
667 if err != nil {
668 http.Error(w, err.Error(), http.StatusInternalServerError)
669 return
670 }
671 data := commitStatusData{
672 Navigation: []navItem{
673 navItem{"Home", "/"},
674 navItem{appName, "/" + appName},
675 navItem{hash, "/" + appName + "/" + hash},
676 },
677 AppName: appName,
678 Commit: commit,
679 Resources: resData,
680 }
681 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
682 http.Error(w, err.Error(), http.StatusInternalServerError)
683 return
684 }
685}
686
gio183e8342024-08-20 06:01:24 +0400687type logData struct {
688 Navigation []navItem
689 AppName string
690 Logs template.HTML
691}
692
gio59946282024-10-07 12:55:51 +0400693func (s *Server) handleAppLogs(w http.ResponseWriter, r *http.Request) {
gio183e8342024-08-20 06:01:24 +0400694 vars := mux.Vars(r)
695 appName, ok := vars["app-name"]
696 if !ok || appName == "" {
697 http.Error(w, "missing app-name", http.StatusBadRequest)
698 return
699 }
700 u := r.Context().Value(userCtx)
701 if u == nil {
702 http.Error(w, "unauthorized", http.StatusUnauthorized)
703 return
704 }
705 user, ok := u.(string)
706 if !ok {
707 http.Error(w, "could not get user", http.StatusInternalServerError)
708 return
709 }
710 owner, err := s.st.GetAppOwner(appName)
711 if err != nil {
712 http.Error(w, err.Error(), http.StatusInternalServerError)
713 return
714 }
715 if owner != user {
716 http.Error(w, "unauthorized", http.StatusUnauthorized)
717 return
718 }
719 data := logData{
720 Navigation: []navItem{
721 navItem{"Home", "/"},
722 navItem{appName, "/" + appName},
723 navItem{"Logs", "/" + appName + "/logs"},
724 },
725 AppName: appName,
726 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
727 }
728 if err := s.tmplts.logs.Execute(w, data); err != nil {
729 fmt.Println(err)
730 http.Error(w, err.Error(), http.StatusInternalServerError)
731 return
732 }
733}
734
gio81246f02024-07-10 12:02:15 +0400735type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400736 Ref string `json:"ref"`
737 Repository struct {
738 Name string `json:"name"`
739 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400740 After string `json:"after"`
741 Commits []struct {
742 Id string `json:"id"`
743 Message string `json:"message"`
744 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400745}
746
gio59946282024-10-07 12:55:51 +0400747func (s *Server) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400748 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400749 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400750 var contents strings.Builder
751 io.Copy(&contents, r.Body)
752 c := contents.String()
753 fmt.Println(c)
754 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400755 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400756 return
757 }
gio7fbd4ad2024-08-27 10:06:39 +0400758 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
759 return
760 }
761 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
762 if !ok {
763 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400764 return
765 }
gioa60f0de2024-07-08 10:49:48 +0400766 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400767 go func() {
gio11617ac2024-07-15 16:09:04 +0400768 owner, err := s.st.GetAppOwner(req.Repository.Name)
769 if err != nil {
770 return
771 }
772 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400773 if err != nil {
774 return
775 }
giof15b9da2024-09-19 06:59:16 +0400776 // TODO(gio): get only available ones by owner
777 clusters, err := s.getClusters()
778 if err != nil {
779 return
780 }
gio94904702024-07-26 16:58:34 +0400781 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
782 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
783 if err != nil {
784 return
785 }
gioe2e31e12024-08-18 08:20:56 +0400786 found := false
787 commitMsg := ""
788 for _, c := range req.Commits {
789 if c.Id == req.After {
790 found = true
791 commitMsg = c.Message
792 break
gioa60f0de2024-07-08 10:49:48 +0400793 }
794 }
gioe2e31e12024-08-18 08:20:56 +0400795 if !found {
796 fmt.Printf("Error: could not find commit message")
797 return
798 }
gio7fbd4ad2024-08-27 10:06:39 +0400799 s.l.Lock()
800 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400801 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 +0400802 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400803 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400804 return
805 }
gioa60f0de2024-07-08 10:49:48 +0400806 for addr, _ := range s.workers[req.Repository.Name] {
807 go func() {
808 // TODO(gio): make port configurable
809 http.Get(fmt.Sprintf("http://%s/update", addr))
810 }()
gio0eaf2712024-04-14 13:08:46 +0400811 }
812 }()
gio0eaf2712024-04-14 13:08:46 +0400813}
814
gio81246f02024-07-10 12:02:15 +0400815type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400816 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400817 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400818}
819
gio59946282024-10-07 12:55:51 +0400820func (s *Server) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400821 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400822 vars := mux.Vars(r)
823 appName, ok := vars["app-name"]
824 if !ok || appName == "" {
825 http.Error(w, "missing app-name", http.StatusBadRequest)
826 return
827 }
gio81246f02024-07-10 12:02:15 +0400828 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400829 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
830 http.Error(w, err.Error(), http.StatusInternalServerError)
831 return
832 }
gioa60f0de2024-07-08 10:49:48 +0400833 if _, ok := s.workers[appName]; !ok {
834 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400835 }
gioa60f0de2024-07-08 10:49:48 +0400836 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400837 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400838}
839
gio59946282024-10-07 12:55:51 +0400840func (s *Server) handleCreateApp(w http.ResponseWriter, r *http.Request) {
gio11617ac2024-07-15 16:09:04 +0400841 u := r.Context().Value(userCtx)
842 if u == nil {
843 http.Error(w, "unauthorized", http.StatusUnauthorized)
844 return
845 }
846 user, ok := u.(string)
847 if !ok {
848 http.Error(w, "could not get user", http.StatusInternalServerError)
849 return
850 }
851 network := r.FormValue("network")
852 if network == "" {
853 http.Error(w, "missing network", http.StatusBadRequest)
854 return
855 }
gio5e49bb62024-07-20 10:43:19 +0400856 subdomain := r.FormValue("subdomain")
857 if subdomain == "" {
858 http.Error(w, "missing subdomain", http.StatusBadRequest)
859 return
860 }
861 appType := r.FormValue("type")
862 if appType == "" {
863 http.Error(w, "missing type", http.StatusBadRequest)
864 return
865 }
gio5cc6afc2024-10-06 09:33:44 +0400866 appName := r.FormValue("name")
867 var err error
868 if appName == "" {
869 g := installer.NewFixedLengthRandomNameGenerator(3)
870 appName, err = g.Generate()
871 }
gio11617ac2024-07-15 16:09:04 +0400872 if err != nil {
873 http.Error(w, err.Error(), http.StatusInternalServerError)
874 return
875 }
876 if ok, err := s.client.UserExists(user); err != nil {
877 http.Error(w, err.Error(), http.StatusInternalServerError)
878 return
879 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400880 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
881 return
gio11617ac2024-07-15 16:09:04 +0400882 }
giocafd4e62024-07-31 10:53:40 +0400883 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400884 http.Error(w, err.Error(), http.StatusInternalServerError)
885 return
886 }
887 if err := s.st.CreateApp(appName, user); err != nil {
888 http.Error(w, err.Error(), http.StatusInternalServerError)
889 return
890 }
giod8ab4f52024-07-26 16:58:34 +0400891 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400892 http.Error(w, err.Error(), http.StatusInternalServerError)
893 return
894 }
895 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
896}
897
gio59946282024-10-07 12:55:51 +0400898func (s *Server) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400899 u := r.Context().Value(userCtx)
900 if u == nil {
901 http.Error(w, "unauthorized", http.StatusUnauthorized)
902 return
903 }
904 user, ok := u.(string)
905 if !ok {
906 http.Error(w, "could not get user", http.StatusInternalServerError)
907 return
908 }
909 vars := mux.Vars(r)
910 appName, ok := vars["app-name"]
911 if !ok || appName == "" {
912 http.Error(w, "missing app-name", http.StatusBadRequest)
913 return
914 }
915 branch := r.FormValue("branch")
916 if branch == "" {
gio5887caa2024-10-03 15:07:23 +0400917 http.Error(w, "missing branch", http.StatusBadRequest)
gio7fbd4ad2024-08-27 10:06:39 +0400918 return
919 }
920 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
921 http.Error(w, err.Error(), http.StatusInternalServerError)
922 return
923 }
924 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
925}
926
gio59946282024-10-07 12:55:51 +0400927func (s *Server) handleBranchDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +0400928 u := r.Context().Value(userCtx)
929 if u == nil {
930 http.Error(w, "unauthorized", http.StatusUnauthorized)
931 return
932 }
933 vars := mux.Vars(r)
934 appName, ok := vars["app-name"]
935 if !ok || appName == "" {
936 http.Error(w, "missing app-name", http.StatusBadRequest)
937 return
938 }
939 branch, ok := vars["branch"]
940 if !ok || branch == "" {
941 http.Error(w, "missing branch", http.StatusBadRequest)
942 return
943 }
944 if err := s.deleteBranch(appName, branch); err != nil {
945 http.Error(w, err.Error(), http.StatusInternalServerError)
946 return
947 }
948 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
949}
950
gio59946282024-10-07 12:55:51 +0400951func (s *Server) handleAppDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +0400952 u := r.Context().Value(userCtx)
953 if u == nil {
954 http.Error(w, "unauthorized", http.StatusUnauthorized)
955 return
956 }
957 vars := mux.Vars(r)
958 appName, ok := vars["app-name"]
959 if !ok || appName == "" {
960 http.Error(w, "missing app-name", http.StatusBadRequest)
961 return
962 }
963 if err := s.deleteApp(appName); err != nil {
964 http.Error(w, err.Error(), http.StatusInternalServerError)
965 return
966 }
gioe44c1512024-10-06 14:13:55 +0400967 http.Redirect(w, r, "/", http.StatusSeeOther)
gio5887caa2024-10-03 15:07:23 +0400968}
969
gio81246f02024-07-10 12:02:15 +0400970type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400971 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400972 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400973 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400974 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400975}
976
gio81246f02024-07-10 12:02:15 +0400977type apiCreateAppResp struct {
978 AppName string `json:"appName"`
979 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400980}
981
gio59946282024-10-07 12:55:51 +0400982func (s *Server) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400983 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400984 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400985 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
986 http.Error(w, err.Error(), http.StatusBadRequest)
987 return
988 }
989 g := installer.NewFixedLengthRandomNameGenerator(3)
990 appName, err := g.Generate()
991 if err != nil {
992 http.Error(w, err.Error(), http.StatusInternalServerError)
993 return
994 }
gio11617ac2024-07-15 16:09:04 +0400995 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400996 if err != nil {
gio33059762024-07-05 13:19:07 +0400997 http.Error(w, err.Error(), http.StatusInternalServerError)
998 return
999 }
gio11617ac2024-07-15 16:09:04 +04001000 if user != "" {
1001 http.Error(w, "public key already registered", http.StatusBadRequest)
1002 return
1003 }
1004 user = appName
1005 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
1006 http.Error(w, err.Error(), http.StatusInternalServerError)
1007 return
1008 }
1009 password := generatePassword()
1010 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1011 if err != nil {
1012 http.Error(w, err.Error(), http.StatusInternalServerError)
1013 return
1014 }
giocafd4e62024-07-31 10:53:40 +04001015 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +04001016 http.Error(w, err.Error(), http.StatusInternalServerError)
1017 return
1018 }
1019 if err := s.st.CreateApp(appName, user); err != nil {
1020 http.Error(w, err.Error(), http.StatusInternalServerError)
1021 return
1022 }
giod8ab4f52024-07-26 16:58:34 +04001023 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +04001024 http.Error(w, err.Error(), http.StatusInternalServerError)
1025 return
1026 }
gio81246f02024-07-10 12:02:15 +04001027 resp := apiCreateAppResp{
1028 AppName: appName,
1029 Password: password,
1030 }
gio33059762024-07-05 13:19:07 +04001031 if err := json.NewEncoder(w).Encode(resp); err != nil {
1032 http.Error(w, err.Error(), http.StatusInternalServerError)
1033 return
1034 }
1035}
1036
gio59946282024-10-07 12:55:51 +04001037func (s *Server) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +04001038 if !s.external {
giod8ab4f52024-07-26 16:58:34 +04001039 return true
1040 }
1041 for _, cfg := range s.appConfigs {
1042 if strings.ToLower(cfg.Network) == network {
1043 return false
1044 }
1045 }
1046 return true
1047}
1048
gio59946282024-10-07 12:55:51 +04001049func (s *Server) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +04001050 s.l.Lock()
1051 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +04001052 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +04001053 network = strings.ToLower(network)
1054 if !s.isNetworkUseAllowed(network) {
1055 return fmt.Errorf("network already used: %s", network)
1056 }
gio33059762024-07-05 13:19:07 +04001057 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001058 return err
gio33059762024-07-05 13:19:07 +04001059 } else if ok {
gio11617ac2024-07-15 16:09:04 +04001060 return nil
gioa60f0de2024-07-08 10:49:48 +04001061 }
gio5e49bb62024-07-20 10:43:19 +04001062 networks, err := s.getNetworks(user)
1063 if err != nil {
1064 return err
1065 }
giod8ab4f52024-07-26 16:58:34 +04001066 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +04001067 if !ok {
1068 return fmt.Errorf("network not found: %s\n", network)
1069 }
gio33059762024-07-05 13:19:07 +04001070 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001071 return err
gio33059762024-07-05 13:19:07 +04001072 }
1073 appRepo, err := s.client.GetRepo(appName)
1074 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001075 return err
gio33059762024-07-05 13:19:07 +04001076 }
gio7fbd4ad2024-08-27 10:06:39 +04001077 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
1078 if err != nil {
1079 return err
1080 }
1081 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1082}
1083
gio59946282024-10-07 12:55:51 +04001084func (s *Server) createDevBranch(appName, fromBranch, toBranch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001085 s.l.Lock()
1086 defer s.l.Unlock()
1087 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1088 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1089 if err != nil {
1090 return err
1091 }
gioc81a8472024-09-24 13:06:19 +02001092 appCfg, err := soft.ReadFile(appRepo, "app.json")
gio7fbd4ad2024-08-27 10:06:39 +04001093 if err != nil {
1094 return err
1095 }
1096 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1097 if err != nil {
1098 return err
1099 }
gioc81a8472024-09-24 13:06:19 +02001100 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
gio7fbd4ad2024-08-27 10:06:39 +04001101}
1102
gio59946282024-10-07 12:55:51 +04001103func (s *Server) deleteBranch(appName string, branch string) error {
gio5887caa2024-10-03 15:07:23 +04001104 appBranch := fmt.Sprintf("dodo_%s", branch)
1105 hf := installer.NewGitHelmFetcher()
1106 if err := func() error {
1107 repo, err := s.client.GetRepoBranch(appName, appBranch)
1108 if err != nil {
1109 return err
1110 }
1111 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
1112 if err != nil {
1113 return err
1114 }
1115 return m.Remove("app")
1116 }(); err != nil {
1117 return err
1118 }
1119 configRepo, err := s.client.GetRepo(ConfigRepoName)
1120 if err != nil {
1121 return err
1122 }
1123 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
1124 if err != nil {
1125 return err
1126 }
1127 appPath := fmt.Sprintf("%s/%s", appName, branch)
gio829b1b72024-10-05 21:50:56 +04001128 if err := m.Remove(appPath); err != nil {
gio5887caa2024-10-03 15:07:23 +04001129 return err
1130 }
1131 if err := s.client.DeleteRepoBranch(appName, appBranch); err != nil {
1132 return err
1133 }
1134 if branch != "master" {
gioe44c1512024-10-06 14:13:55 +04001135 if err := s.client.DeleteRepoBranch(appName, branch); err != nil {
1136 return err
1137 }
gio5887caa2024-10-03 15:07:23 +04001138 }
gioe44c1512024-10-06 14:13:55 +04001139 return s.st.DeleteBranch(appName, branch)
gio5887caa2024-10-03 15:07:23 +04001140}
1141
gio59946282024-10-07 12:55:51 +04001142func (s *Server) deleteApp(appName string) error {
gio5887caa2024-10-03 15:07:23 +04001143 configRepo, err := s.client.GetRepo(ConfigRepoName)
1144 if err != nil {
1145 return err
1146 }
1147 branches, err := configRepo.ListDir(fmt.Sprintf("/%s", appName))
1148 if err != nil {
1149 return err
1150 }
1151 for _, b := range branches {
1152 if !b.IsDir() || strings.HasPrefix(b.Name(), "dodo_") {
1153 continue
1154 }
1155 if err := s.deleteBranch(appName, b.Name()); err != nil {
1156 return err
1157 }
1158 }
gioe44c1512024-10-06 14:13:55 +04001159 if err := s.client.DeleteRepo(appName); err != nil {
1160 return err
1161 }
1162 return s.st.DeleteApp(appName)
gio5887caa2024-10-03 15:07:23 +04001163}
1164
gio59946282024-10-07 12:55:51 +04001165func (s *Server) createAppForBranch(
gio7fbd4ad2024-08-27 10:06:39 +04001166 repo soft.RepoIO,
1167 appName string,
1168 branch string,
1169 user string,
1170 network string,
1171 files map[string][]byte,
1172) error {
1173 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1174 for path, contents := range files {
1175 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1176 return "", err
1177 }
1178 }
1179 return "init", nil
1180 }, soft.WithCommitToBranch(branch))
1181 if err != nil {
1182 return err
1183 }
1184 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001185 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001186 return err
gio33059762024-07-05 13:19:07 +04001187 }
giof15b9da2024-09-19 06:59:16 +04001188 // TODO(gio): get only available ones by owner
1189 clusters, err := s.getClusters()
1190 if err != nil {
1191 return err
1192 }
gio33059762024-07-05 13:19:07 +04001193 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001194 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1195 if err != nil {
1196 return err
1197 }
1198 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001199 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001200 return err
gio33059762024-07-05 13:19:07 +04001201 }
1202 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1203 suffix, err := suffixGen.Generate()
1204 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001205 return err
gio33059762024-07-05 13:19:07 +04001206 }
gio94904702024-07-26 16:58:34 +04001207 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001208 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001209 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001210 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001211 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001212 return err
1213 }
gio7fbd4ad2024-08-27 10:06:39 +04001214 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001215 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001216 return err
gio33059762024-07-05 13:19:07 +04001217 }
giod8ab4f52024-07-26 16:58:34 +04001218 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001219 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001220 return err
gio33059762024-07-05 13:19:07 +04001221 }
1222 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001223 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001224 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001225 return err
gio33059762024-07-05 13:19:07 +04001226 }
gio7fbd4ad2024-08-27 10:06:39 +04001227 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001228 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001229 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001230 if err != nil {
1231 return "", err
1232 }
1233 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001234 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001235 return "", err
1236 }
1237 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001238 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001239 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001240 appPath,
gio9d66f322024-07-06 13:45:10 +04001241 namespace,
1242 map[string]any{
1243 "repoAddr": s.client.GetRepoAddress(appName),
1244 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001245 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001246 "gitRepoPublicKey": s.gitRepoPublicKey,
1247 },
1248 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001249 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001250 installer.WithNoPublish(),
1251 installer.WithNoLock(),
1252 ); err != nil {
1253 return "", err
1254 }
1255 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001256 })
1257 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001258 return err
gio33059762024-07-05 13:19:07 +04001259 }
gio7fbd4ad2024-08-27 10:06:39 +04001260 return s.initAppACLs(m, appPath, appName, branch, user)
1261}
1262
gio59946282024-10-07 12:55:51 +04001263func (s *Server) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001264 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001265 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001266 return err
gio33059762024-07-05 13:19:07 +04001267 }
1268 fluxKeys, ok := cfg.Input["fluxKeys"]
1269 if !ok {
gio11617ac2024-07-15 16:09:04 +04001270 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001271 }
1272 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1273 if !ok {
gio11617ac2024-07-15 16:09:04 +04001274 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001275 }
1276 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001277 return err
gio33059762024-07-05 13:19:07 +04001278 } else if ok {
1279 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001280 return err
gio33059762024-07-05 13:19:07 +04001281 }
1282 } else {
1283 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001284 return err
gio33059762024-07-05 13:19:07 +04001285 }
1286 }
gio7fbd4ad2024-08-27 10:06:39 +04001287 if branch != "master" {
1288 return nil
1289 }
gio33059762024-07-05 13:19:07 +04001290 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001291 return err
gio33059762024-07-05 13:19:07 +04001292 }
gio7fbd4ad2024-08-27 10:06:39 +04001293 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001294 return err
gio33059762024-07-05 13:19:07 +04001295 }
gio7fbd4ad2024-08-27 10:06:39 +04001296 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 +04001297 return err
gio33059762024-07-05 13:19:07 +04001298 }
gio2ccb6e32024-08-15 12:01:33 +04001299 if !s.external {
1300 go func() {
1301 users, err := s.client.GetAllUsers()
1302 if err != nil {
1303 fmt.Println(err)
1304 return
1305 }
1306 for _, user := range users {
1307 // TODO(gio): fluxcd should have only read access
1308 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1309 fmt.Println(err)
1310 }
1311 }
1312 }()
1313 }
gio43b0f422024-08-21 10:40:13 +04001314 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1315 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001316 return nil
gio33059762024-07-05 13:19:07 +04001317}
1318
gio81246f02024-07-10 12:02:15 +04001319type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001320 User string `json:"user"`
1321 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001322}
1323
gio59946282024-10-07 12:55:51 +04001324func (s *Server) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001325 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001326 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1327 http.Error(w, err.Error(), http.StatusBadRequest)
1328 return
1329 }
gio7fbd4ad2024-08-27 10:06:39 +04001330 if req.User == "" {
1331 http.Error(w, "invalid user", http.StatusBadRequest)
1332 return
1333 }
1334 if req.PublicKey == "" {
1335 http.Error(w, "invalid public key", http.StatusBadRequest)
1336 return
1337 }
1338 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001339 http.Error(w, err.Error(), http.StatusInternalServerError)
1340 return
1341 }
1342}
1343
gio94904702024-07-26 16:58:34 +04001344type dodoAppRendered struct {
1345 App struct {
1346 Ingress struct {
1347 Network string `json:"network"`
1348 Subdomain string `json:"subdomain"`
1349 } `json:"ingress"`
1350 } `json:"app"`
1351 Input struct {
1352 AppId string `json:"appId"`
1353 } `json:"input"`
1354}
1355
gio7fbd4ad2024-08-27 10:06:39 +04001356// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio59946282024-10-07 12:55:51 +04001357func (s *Server) updateDodoApp(
gio43b0f422024-08-21 10:40:13 +04001358 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001359 name string,
1360 branch string,
1361 namespace string,
gio43b0f422024-08-21 10:40:13 +04001362 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001363 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001364 owner string,
gio43b0f422024-08-21 10:40:13 +04001365) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001366 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001367 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001368 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001369 }
giof8843412024-05-22 16:38:05 +04001370 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001371 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001372 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001373 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001374 }
gioc81a8472024-09-24 13:06:19 +02001375 appCfg, err := soft.ReadFile(repo, "app.json")
gio0eaf2712024-04-14 13:08:46 +04001376 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001377 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001378 }
1379 app, err := installer.NewDodoApp(appCfg)
1380 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001381 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001382 }
giof8843412024-05-22 16:38:05 +04001383 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001384 var ret installer.ReleaseResources
1385 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1386 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001387 app,
1388 "app",
1389 "/.dodo/app",
1390 namespace,
1391 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001392 "repoAddr": repo.FullAddress(),
1393 "repoPublicAddr": s.repoPublicAddr,
1394 "managerAddr": fmt.Sprintf("http://%s", s.self),
1395 "appId": name,
1396 "branch": branch,
1397 "sshPrivateKey": s.sshKey,
1398 "username": owner,
gio94904702024-07-26 16:58:34 +04001399 },
1400 installer.WithNoPull(),
1401 installer.WithNoPublish(),
1402 installer.WithConfig(&s.env),
1403 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001404 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001405 installer.WithLocalChartGenerator(lg),
1406 installer.WithNoLock(),
1407 )
1408 if err != nil {
1409 return "", err
1410 }
1411 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001412 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001413 return "", nil
1414 }
1415 if _, err := m.Install(
1416 appStatus,
1417 "status",
1418 "/.dodo/status",
1419 s.namespace,
1420 map[string]any{
1421 "appName": rendered.Input.AppId,
1422 "network": rendered.App.Ingress.Network,
1423 "appSubdomain": rendered.App.Ingress.Subdomain,
1424 },
1425 installer.WithNoPull(),
1426 installer.WithNoPublish(),
1427 installer.WithConfig(&s.env),
1428 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001429 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001430 installer.WithLocalChartGenerator(lg),
1431 installer.WithNoLock(),
1432 ); err != nil {
1433 return "", err
1434 }
1435 return "install app", nil
1436 },
gio7fbd4ad2024-08-27 10:06:39 +04001437 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001438 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001439 ); err != nil {
1440 return installer.ReleaseResources{}, err
1441 }
gio43b0f422024-08-21 10:40:13 +04001442 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1443 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001444 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001445}
gio33059762024-07-05 13:19:07 +04001446
gio59946282024-10-07 12:55:51 +04001447func (s *Server) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001448 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001449 appTmpl, err := s.appTmpls.Find(appType)
1450 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001451 return nil, err
gio33059762024-07-05 13:19:07 +04001452 }
giod99b2bd2024-10-09 18:29:15 +04001453 return appTmpl.Render(fmt.Sprintf("%s/schemas/app.schema.json", s.selfPublic), network, subdomain)
gio33059762024-07-05 13:19:07 +04001454}
gio81246f02024-07-10 12:02:15 +04001455
1456func generatePassword() string {
1457 return "foo"
1458}
giocb34ad22024-07-11 08:01:13 +04001459
gio59946282024-10-07 12:55:51 +04001460func (s *Server) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001461 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001462 resp, err := http.Get(addr)
1463 if err != nil {
1464 return nil, err
1465 }
gio23bdc1b2024-07-11 16:07:47 +04001466 networks := []installer.Network{}
1467 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001468 return nil, err
1469 }
gio11617ac2024-07-15 16:09:04 +04001470 return s.nf.Filter(user, networks)
1471}
1472
gio59946282024-10-07 12:55:51 +04001473func (s *Server) getClusters() ([]installer.Cluster, error) {
giof15b9da2024-09-19 06:59:16 +04001474 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1475 resp, err := http.Get(addr)
1476 if err != nil {
1477 return nil, err
1478 }
1479 clusters := []installer.Cluster{}
1480 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1481 return nil, err
1482 }
1483 fmt.Printf("CLUSTERS %+v\n", clusters)
1484 return clusters, nil
1485}
1486
gio8fae3af2024-07-25 13:43:31 +04001487type publicNetworkData struct {
1488 Name string `json:"name"`
1489 Domain string `json:"domain"`
1490}
1491
1492type publicData struct {
1493 Networks []publicNetworkData `json:"networks"`
1494 Types []string `json:"types"`
1495}
1496
gio59946282024-10-07 12:55:51 +04001497func (s *Server) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001498 w.Header().Set("Access-Control-Allow-Origin", "*")
1499 s.l.Lock()
1500 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001501 networks, err := s.getNetworks("")
1502 if err != nil {
1503 http.Error(w, err.Error(), http.StatusInternalServerError)
1504 return
1505 }
1506 var ret publicData
1507 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001508 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1509 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1510 }
gio8fae3af2024-07-25 13:43:31 +04001511 }
1512 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001513 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001514 }
gio8fae3af2024-07-25 13:43:31 +04001515 if err := json.NewEncoder(w).Encode(ret); err != nil {
1516 http.Error(w, err.Error(), http.StatusInternalServerError)
1517 return
1518 }
1519}
1520
gio59946282024-10-07 12:55:51 +04001521func (s *Server) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001522 if err != nil {
1523 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001524 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001525 fmt.Printf("Error: %s\n", err.Error())
1526 return err
1527 }
1528 return err
1529 }
1530 var resB bytes.Buffer
1531 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001532 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001533 fmt.Printf("Error: %s\n", err.Error())
1534 return err
1535 }
1536 return err
1537 }
gio7fbd4ad2024-08-27 10:06:39 +04001538 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001539 fmt.Printf("Error: %s\n", err.Error())
1540 return err
1541 }
1542 return nil
1543}
1544
gio11617ac2024-07-15 16:09:04 +04001545func pickNetwork(networks []installer.Network, network string) []installer.Network {
1546 for _, n := range networks {
1547 if n.Name == network {
1548 return []installer.Network{n}
1549 }
1550 }
1551 return []installer.Network{}
1552}
1553
1554type NetworkFilter interface {
1555 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1556}
1557
1558type noNetworkFilter struct{}
1559
1560func NewNoNetworkFilter() NetworkFilter {
1561 return noNetworkFilter{}
1562}
1563
gio8fae3af2024-07-25 13:43:31 +04001564func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001565 return networks, nil
1566}
1567
1568type filterByOwner struct {
1569 st Store
1570}
1571
1572func NewNetworkFilterByOwner(st Store) NetworkFilter {
1573 return &filterByOwner{st}
1574}
1575
1576func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001577 if user == "" {
1578 return networks, nil
1579 }
gio11617ac2024-07-15 16:09:04 +04001580 network, err := f.st.GetUserNetwork(user)
1581 if err != nil {
1582 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001583 }
1584 ret := []installer.Network{}
1585 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001586 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001587 ret = append(ret, n)
1588 }
1589 }
giocb34ad22024-07-11 08:01:13 +04001590 return ret, nil
1591}
gio11617ac2024-07-15 16:09:04 +04001592
1593type allowListFilter struct {
1594 allowed []string
1595}
1596
1597func NewAllowListFilter(allowed []string) NetworkFilter {
1598 return &allowListFilter{allowed}
1599}
1600
gio8fae3af2024-07-25 13:43:31 +04001601func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001602 ret := []installer.Network{}
1603 for _, n := range networks {
1604 if slices.Contains(f.allowed, n.Name) {
1605 ret = append(ret, n)
1606 }
1607 }
1608 return ret, nil
1609}
1610
1611type combinedNetworkFilter struct {
1612 filters []NetworkFilter
1613}
1614
1615func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1616 return &combinedNetworkFilter{filters}
1617}
1618
gio8fae3af2024-07-25 13:43:31 +04001619func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001620 ret := networks
1621 var err error
1622 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001623 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001624 if err != nil {
1625 return nil, err
1626 }
1627 }
1628 return ret, nil
1629}
giocafd4e62024-07-31 10:53:40 +04001630
1631type user struct {
1632 Username string `json:"username"`
1633 Email string `json:"email"`
1634 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1635}
1636
gio59946282024-10-07 12:55:51 +04001637func (s *Server) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
giocafd4e62024-07-31 10:53:40 +04001638 go s.syncUsers()
1639}
1640
gio59946282024-10-07 12:55:51 +04001641func (s *Server) syncUsers() {
giocafd4e62024-07-31 10:53:40 +04001642 if s.external {
1643 panic("MUST NOT REACH!")
1644 }
1645 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1646 if err != nil {
1647 return
1648 }
1649 users := []user{}
1650 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1651 fmt.Println(err)
1652 return
1653 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001654 validUsernames := make(map[string]user)
1655 for _, u := range users {
1656 validUsernames[u.Username] = u
1657 }
1658 allClientUsers, err := s.client.GetAllUsers()
1659 if err != nil {
1660 fmt.Println(err)
1661 return
1662 }
1663 keyToUser := make(map[string]string)
1664 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001665 if clientUser == "admin" || clientUser == "fluxcd" {
1666 continue
1667 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001668 userData, ok := validUsernames[clientUser]
1669 if !ok {
1670 if err := s.client.RemoveUser(clientUser); err != nil {
1671 fmt.Println(err)
1672 return
1673 }
1674 } else {
1675 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1676 if err != nil {
1677 fmt.Println(err)
1678 return
1679 }
1680 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001681 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001682 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001683 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001684 })
1685 if !keyOk {
1686 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1687 fmt.Println(err)
1688 }
1689 } else {
1690 keyToUser[cleanKey] = clientUser
1691 }
1692 }
1693 }
1694 }
giocafd4e62024-07-31 10:53:40 +04001695 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001696 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1697 fmt.Println(err)
1698 return
1699 }
giocafd4e62024-07-31 10:53:40 +04001700 if len(u.SSHPublicKeys) == 0 {
1701 continue
1702 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001703 ok, err := s.client.UserExists(u.Username)
1704 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001705 fmt.Println(err)
1706 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001707 }
1708 if !ok {
1709 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1710 fmt.Println(err)
1711 return
1712 }
1713 } else {
1714 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001715 cleanKey := soft.CleanKey(key)
1716 if user, ok := keyToUser[cleanKey]; ok {
1717 if u.Username != user {
1718 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1719 }
1720 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001721 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001722 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001723 fmt.Println(err)
1724 return
giocafd4e62024-07-31 10:53:40 +04001725 }
1726 }
1727 }
1728 }
1729 repos, err := s.client.GetAllRepos()
1730 if err != nil {
1731 return
1732 }
1733 for _, r := range repos {
1734 if r == ConfigRepoName {
1735 continue
1736 }
1737 for _, u := range users {
1738 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1739 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001740 continue
giocafd4e62024-07-31 10:53:40 +04001741 }
1742 }
1743 }
1744}
giob4a3a192024-08-19 09:55:47 +04001745
1746func extractResourceData(resources []installer.Resource) (resourceData, error) {
1747 var ret resourceData
1748 for _, r := range resources {
1749 t, ok := r.Annotations["dodo.cloud/resource-type"]
1750 if !ok {
1751 continue
1752 }
giof078f462024-10-14 09:07:33 +04001753 internal, ok := r.Annotations["dodo.cloud/internal"]
1754 if ok && strings.ToLower(internal) == "true" {
1755 continue
1756 }
giob4a3a192024-08-19 09:55:47 +04001757 switch t {
1758 case "volume":
1759 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1760 if !ok {
1761 return resourceData{}, fmt.Errorf("no name")
1762 }
1763 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1764 if !ok {
1765 return resourceData{}, fmt.Errorf("no size")
1766 }
1767 ret.Volume = append(ret.Volume, volume{name, size})
1768 case "postgresql":
1769 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1770 if !ok {
1771 return resourceData{}, fmt.Errorf("no name")
1772 }
1773 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1774 if !ok {
1775 return resourceData{}, fmt.Errorf("no version")
1776 }
1777 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1778 if !ok {
1779 return resourceData{}, fmt.Errorf("no volume")
1780 }
1781 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1782 case "ingress":
giof078f462024-10-14 09:07:33 +04001783 name, ok := r.Annotations["dodo.cloud/resource.ingress.name"]
1784 if !ok {
1785 return resourceData{}, fmt.Errorf("no name")
1786 }
1787 home, ok := r.Annotations["dodo.cloud/resource.ingress.home"]
1788 if !ok {
1789 home = ""
1790 }
giob4a3a192024-08-19 09:55:47 +04001791 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1792 if !ok {
1793 return resourceData{}, fmt.Errorf("no host")
1794 }
giof078f462024-10-14 09:07:33 +04001795 ret.Ingress = append(ret.Ingress, ingress{name, host, home})
gio7fbd4ad2024-08-27 10:06:39 +04001796 case "virtual-machine":
1797 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1798 if !ok {
1799 return resourceData{}, fmt.Errorf("no name")
1800 }
1801 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1802 if !ok {
1803 return resourceData{}, fmt.Errorf("no user")
1804 }
1805 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1806 if !ok {
1807 return resourceData{}, fmt.Errorf("no cpu cores")
1808 }
1809 cpuCores, err := strconv.Atoi(cpuCoresS)
1810 if err != nil {
1811 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1812 }
1813 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1814 if !ok {
1815 return resourceData{}, fmt.Errorf("no memory")
1816 }
1817 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001818 default:
1819 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1820 }
1821 }
giof078f462024-10-14 09:07:33 +04001822 sort.Slice(ret.Ingress, func(i, j int) bool {
1823 return strings.Compare(ret.Ingress[i].Name, ret.Ingress[j].Name) < 0
1824 })
giob4a3a192024-08-19 09:55:47 +04001825 return ret, nil
1826}
gio7fbd4ad2024-08-27 10:06:39 +04001827
1828func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
gioc81a8472024-09-24 13:06:19 +02001829 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
1830 "app.cue": from,
1831 })
gio7fbd4ad2024-08-27 10:06:39 +04001832 if err != nil {
1833 return "", nil, err
1834 }
1835 if err := cfg.Err(); err != nil {
1836 return "", nil, err
1837 }
1838 if err := cfg.Validate(); err != nil {
1839 return "", nil, err
1840 }
1841 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1842 if err := subdomain.Err(); err != nil {
1843 return "", nil, err
1844 }
1845 subdomainStr, err := subdomain.String()
1846 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1847 if err := network.Err(); err != nil {
1848 return "", nil, err
1849 }
1850 networkStr, err := network.String()
1851 if err != nil {
1852 return "", nil, err
1853 }
1854 newCfg := map[string]any{}
1855 if err := cfg.Decode(&newCfg); err != nil {
1856 return "", nil, err
1857 }
1858 app, ok := newCfg["app"].(map[string]any)
1859 if !ok {
1860 return "", nil, fmt.Errorf("not a map")
1861 }
1862 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1863 app["dev"] = map[string]any{
1864 "enabled": true,
1865 "username": username,
1866 }
1867 buf, err := json.MarshalIndent(newCfg, "", "\t")
1868 if err != nil {
1869 return "", nil, err
1870 }
1871 return networkStr, buf, nil
1872}