blob: f921a090f7b1c437d47597ab9ac68c9b7bfa114a [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"
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"
gio59946282024-10-07 12:55:51 +040024 "github.com/giolekva/pcloud/core/installer/server"
gio0eaf2712024-04-14 13:08:46 +040025 "github.com/giolekva/pcloud/core/installer/soft"
gio43b0f422024-08-21 10:40:13 +040026 "github.com/giolekva/pcloud/core/installer/tasks"
gio33059762024-07-05 13:19:07 +040027
gio7fbd4ad2024-08-27 10:06:39 +040028 "cuelang.org/go/cue"
gio33059762024-07-05 13:19:07 +040029 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040030 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040031)
32
gio59946282024-10-07 12:55:51 +040033//go:embed templates/*
34var templates embed.FS
gio23bdc1b2024-07-11 16:07:47 +040035
gio59946282024-10-07 12:55:51 +040036//go:embed all:app-templates
gio5e49bb62024-07-20 10:43:19 +040037var appTmplsFS embed.FS
38
gio59946282024-10-07 12:55:51 +040039//go:embed static/*
40var staticAssets embed.FS
41
42//go:embed static/schemas/app.schema.json
gioc81a8472024-09-24 13:06:19 +020043var dodoAppJsonSchema []byte
44
gio9d66f322024-07-06 13:45:10 +040045const (
gioa60f0de2024-07-08 10:49:48 +040046 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040047 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040048 loginPath = "/login"
49 logoutPath = "/logout"
gio59946282024-10-07 12:55:51 +040050 staticPath = "/static/"
gioc81a8472024-09-24 13:06:19 +020051 schemasPath = "/schemas/"
gio8fae3af2024-07-25 13:43:31 +040052 apiPublicData = "/api/public-data"
53 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040054 sessionCookie = "dodo-app-session"
55 userCtx = "user"
giob4a3a192024-08-19 09:55:47 +040056 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040057)
58
gio59946282024-10-07 12:55:51 +040059type tmplts struct {
giob4a3a192024-08-19 09:55:47 +040060 index *template.Template
61 appStatus *template.Template
62 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040063 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040064}
65
gio59946282024-10-07 12:55:51 +040066func parseTemplates(fs embed.FS) (tmplts, error) {
67 base, err := template.ParseFS(fs, "templates/base.html")
gio23bdc1b2024-07-11 16:07:47 +040068 if err != nil {
gio59946282024-10-07 12:55:51 +040069 return tmplts{}, err
gio23bdc1b2024-07-11 16:07:47 +040070 }
gio5e49bb62024-07-20 10:43:19 +040071 parse := func(path string) (*template.Template, error) {
72 if b, err := base.Clone(); err != nil {
73 return nil, err
74 } else {
75 return b.ParseFS(fs, path)
76 }
77 }
gio59946282024-10-07 12:55:51 +040078 index, err := parse("templates/index.html")
gio5e49bb62024-07-20 10:43:19 +040079 if err != nil {
gio59946282024-10-07 12:55:51 +040080 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040081 }
gio59946282024-10-07 12:55:51 +040082 appStatus, err := parse("templates/app_status.html")
gio5e49bb62024-07-20 10:43:19 +040083 if err != nil {
gio59946282024-10-07 12:55:51 +040084 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040085 }
gio59946282024-10-07 12:55:51 +040086 commitStatus, err := parse("templates/commit_status.html")
giob4a3a192024-08-19 09:55:47 +040087 if err != nil {
gio59946282024-10-07 12:55:51 +040088 return tmplts{}, err
giob4a3a192024-08-19 09:55:47 +040089 }
gio59946282024-10-07 12:55:51 +040090 logs, err := parse("templates/logs.html")
gio183e8342024-08-20 06:01:24 +040091 if err != nil {
gio59946282024-10-07 12:55:51 +040092 return tmplts{}, err
gio183e8342024-08-20 06:01:24 +040093 }
gio59946282024-10-07 12:55:51 +040094 return tmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040095}
96
gio59946282024-10-07 12:55:51 +040097type Server struct {
giocb34ad22024-07-11 08:01:13 +040098 l sync.Locker
99 st Store
gio11617ac2024-07-15 16:09:04 +0400100 nf NetworkFilter
101 ug UserGetter
giocb34ad22024-07-11 08:01:13 +0400102 port int
103 apiPort int
104 self string
gioc81a8472024-09-24 13:06:19 +0200105 selfPublic string
gio11617ac2024-07-15 16:09:04 +0400106 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +0400107 sshKey string
108 gitRepoPublicKey string
109 client soft.Client
110 namespace string
111 envAppManagerAddr string
112 env installer.EnvConfig
113 nsc installer.NamespaceCreator
114 jc installer.JobCreator
gio864b4332024-09-05 13:56:47 +0400115 vpnKeyGen installer.VPNAPIClient
giof6ad2982024-08-23 17:42:49 +0400116 cnc installer.ClusterNetworkConfigurator
giocb34ad22024-07-11 08:01:13 +0400117 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400118 appConfigs map[string]appConfig
gio59946282024-10-07 12:55:51 +0400119 tmplts tmplts
gio5e49bb62024-07-20 10:43:19 +0400120 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400121 external bool
122 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400123 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400124 logs map[string]string
giod8ab4f52024-07-26 16:58:34 +0400125}
126
127type appConfig struct {
128 Namespace string `json:"namespace"`
129 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400130}
131
gio33059762024-07-05 13:19:07 +0400132// TODO(gio): Initialize appNs on startup
gio59946282024-10-07 12:55:51 +0400133func NewServer(
gioa60f0de2024-07-08 10:49:48 +0400134 st Store,
gio11617ac2024-07-15 16:09:04 +0400135 nf NetworkFilter,
136 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400137 port int,
gioa60f0de2024-07-08 10:49:48 +0400138 apiPort int,
gio33059762024-07-05 13:19:07 +0400139 self string,
gioc81a8472024-09-24 13:06:19 +0200140 selfPublic string,
gio11617ac2024-07-15 16:09:04 +0400141 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400142 sshKey string,
gio33059762024-07-05 13:19:07 +0400143 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400144 client soft.Client,
145 namespace string,
giocb34ad22024-07-11 08:01:13 +0400146 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400147 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400148 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400149 vpnKeyGen installer.VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +0400150 cnc installer.ClusterNetworkConfigurator,
gio0eaf2712024-04-14 13:08:46 +0400151 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400152 external bool,
153 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400154 reconciler tasks.Reconciler,
gio59946282024-10-07 12:55:51 +0400155) (*Server, error) {
156 tmplts, err := parseTemplates(templates)
gio23bdc1b2024-07-11 16:07:47 +0400157 if err != nil {
158 return nil, err
159 }
gio5e4d1a72024-10-09 15:25:29 +0400160 apps, err := fs.Sub(appTmplsFS, "app-templates")
gio5e49bb62024-07-20 10:43:19 +0400161 if err != nil {
162 return nil, err
163 }
164 appTmpls, err := NewAppTmplStoreFS(apps)
165 if err != nil {
166 return nil, err
167 }
gio59946282024-10-07 12:55:51 +0400168 s := &Server{
gio9d66f322024-07-06 13:45:10 +0400169 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400170 st,
gio11617ac2024-07-15 16:09:04 +0400171 nf,
172 ug,
gio0eaf2712024-04-14 13:08:46 +0400173 port,
gioa60f0de2024-07-08 10:49:48 +0400174 apiPort,
gio33059762024-07-05 13:19:07 +0400175 self,
gioc81a8472024-09-24 13:06:19 +0200176 selfPublic,
gio11617ac2024-07-15 16:09:04 +0400177 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400178 sshKey,
gio33059762024-07-05 13:19:07 +0400179 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400180 client,
181 namespace,
giocb34ad22024-07-11 08:01:13 +0400182 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400183 env,
gio33059762024-07-05 13:19:07 +0400184 nsc,
giof8843412024-05-22 16:38:05 +0400185 jc,
gio36b23b32024-08-25 12:20:54 +0400186 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +0400187 cnc,
gio266c04f2024-07-03 14:18:45 +0400188 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400189 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400190 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400191 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400192 external,
193 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400194 reconciler,
gio183e8342024-08-20 06:01:24 +0400195 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +0400196 }
gioa60f0de2024-07-08 10:49:48 +0400197 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400198 if err != nil {
199 return nil, err
200 }
giod8ab4f52024-07-26 16:58:34 +0400201 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400202 if err == nil {
203 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400204 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400205 return nil, err
206 }
207 } else if !errors.Is(err, fs.ErrNotExist) {
208 return nil, err
209 }
210 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400211}
212
gio59946282024-10-07 12:55:51 +0400213func (s *Server) getAppConfig(app, branch string) appConfig {
gio7fbd4ad2024-08-27 10:06:39 +0400214 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
215}
216
gio59946282024-10-07 12:55:51 +0400217func (s *Server) setAppConfig(app, branch string, cfg appConfig) {
gio7fbd4ad2024-08-27 10:06:39 +0400218 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
219}
220
gio59946282024-10-07 12:55:51 +0400221func (s *Server) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400222 // if err := s.client.DisableKeyless(); err != nil {
223 // return err
224 // }
225 // if err := s.client.DisableAnonAccess(); err != nil {
226 // return err
227 // }
gioa60f0de2024-07-08 10:49:48 +0400228 e := make(chan error)
229 go func() {
230 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400231 r.Use(s.mwAuth)
gioc81a8472024-09-24 13:06:19 +0200232 r.HandleFunc(schemasPath+"app.schema.json", s.handleSchema).Methods(http.MethodGet)
gio59946282024-10-07 12:55:51 +0400233 r.PathPrefix(staticPath).Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
gio81246f02024-07-10 12:02:15 +0400234 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400235 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
236 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400237 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
238 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400239 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400240 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400241 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
242 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400243 r.HandleFunc("/{app-name}/branch/{branch}/delete", s.handleBranchDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400244 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400245 r.HandleFunc("/{app-name}/delete", s.handleAppDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400246 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400247 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400248 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
249 }()
250 go func() {
251 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400252 r.HandleFunc("/update", s.handleAPIUpdate)
253 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400254 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400255 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400256 if !s.external {
257 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
258 }
gioa60f0de2024-07-08 10:49:48 +0400259 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
260 }()
giocafd4e62024-07-31 10:53:40 +0400261 if !s.external {
262 go func() {
263 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400264 for {
265 delay := time.Duration(rand.Intn(60)+60) * time.Second
266 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400267 s.syncUsers()
268 }
269 }()
270 }
gioa60f0de2024-07-08 10:49:48 +0400271 return <-e
272}
273
gio11617ac2024-07-15 16:09:04 +0400274type UserGetter interface {
275 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400276 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400277}
278
279type externalUserGetter struct {
280 sc *securecookie.SecureCookie
281}
282
283func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400284 return &externalUserGetter{securecookie.New(
285 securecookie.GenerateRandomKey(64),
286 securecookie.GenerateRandomKey(32),
287 )}
gio11617ac2024-07-15 16:09:04 +0400288}
289
290func (ug *externalUserGetter) Get(r *http.Request) string {
291 cookie, err := r.Cookie(sessionCookie)
292 if err != nil {
293 return ""
294 }
295 var user string
296 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
297 return ""
298 }
299 return user
300}
301
gio8fae3af2024-07-25 13:43:31 +0400302func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
303 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
304 cookie := &http.Cookie{
305 Name: sessionCookie,
306 Value: encoded,
307 Path: "/",
308 Secure: true,
309 HttpOnly: true,
310 }
311 http.SetCookie(w, cookie)
312 return nil
313 } else {
314 return err
315 }
316}
317
gio11617ac2024-07-15 16:09:04 +0400318type internalUserGetter struct{}
319
320func NewInternalUserGetter() UserGetter {
321 return internalUserGetter{}
322}
323
324func (ug internalUserGetter) Get(r *http.Request) string {
giodd213152024-09-27 11:26:59 +0200325 return r.Header.Get("X-Forwarded-User")
gio11617ac2024-07-15 16:09:04 +0400326}
327
gio8fae3af2024-07-25 13:43:31 +0400328func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
329 return nil
330}
331
gio59946282024-10-07 12:55:51 +0400332func (s *Server) mwAuth(next http.Handler) http.Handler {
gio81246f02024-07-10 12:02:15 +0400333 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400334 if strings.HasSuffix(r.URL.Path, loginPath) ||
335 strings.HasPrefix(r.URL.Path, logoutPath) ||
336 strings.HasPrefix(r.URL.Path, staticPath) ||
gioc81a8472024-09-24 13:06:19 +0200337 strings.HasPrefix(r.URL.Path, schemasPath) ||
gio8fae3af2024-07-25 13:43:31 +0400338 strings.HasPrefix(r.URL.Path, apiPublicData) ||
339 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400340 next.ServeHTTP(w, r)
341 return
342 }
gio11617ac2024-07-15 16:09:04 +0400343 user := s.ug.Get(r)
344 if user == "" {
gio81246f02024-07-10 12:02:15 +0400345 vars := mux.Vars(r)
346 appName, ok := vars["app-name"]
347 if !ok || appName == "" {
348 http.Error(w, "missing app-name", http.StatusBadRequest)
349 return
350 }
351 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
352 return
353 }
gio81246f02024-07-10 12:02:15 +0400354 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
355 })
356}
357
gio59946282024-10-07 12:55:51 +0400358func (s *Server) handleSchema(w http.ResponseWriter, r *http.Request) {
gioc81a8472024-09-24 13:06:19 +0200359 w.Header().Set("Content-Type", "application/schema+json")
360 w.Write(dodoAppJsonSchema)
361}
362
gio59946282024-10-07 12:55:51 +0400363func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400364 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400365 http.SetCookie(w, &http.Cookie{
366 Name: sessionCookie,
367 Value: "",
368 Path: "/",
369 HttpOnly: true,
370 Secure: true,
371 })
372 http.Redirect(w, r, "/", http.StatusSeeOther)
373}
374
gio59946282024-10-07 12:55:51 +0400375func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400376 vars := mux.Vars(r)
377 appName, ok := vars["app-name"]
378 if !ok || appName == "" {
379 http.Error(w, "missing app-name", http.StatusBadRequest)
380 return
381 }
382 fmt.Fprint(w, `
383<!DOCTYPE html>
384<html lang='en'>
385 <head>
386 <title>dodo: app - login</title>
387 <meta charset='utf-8'>
388 </head>
389 <body>
390 <form action="" method="POST">
391 <input type="password" placeholder="Password" name="password" required />
392 <button type="submit">Login</button>
393 </form>
394 </body>
395</html>
396`)
397}
398
gio59946282024-10-07 12:55:51 +0400399func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400400 vars := mux.Vars(r)
401 appName, ok := vars["app-name"]
402 if !ok || appName == "" {
403 http.Error(w, "missing app-name", http.StatusBadRequest)
404 return
405 }
406 password := r.FormValue("password")
407 if password == "" {
408 http.Error(w, "missing password", http.StatusBadRequest)
409 return
410 }
411 user, err := s.st.GetAppOwner(appName)
412 if err != nil {
413 http.Error(w, err.Error(), http.StatusInternalServerError)
414 return
415 }
416 hashed, err := s.st.GetUserPassword(user)
417 if err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
422 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
423 return
424 }
gio8fae3af2024-07-25 13:43:31 +0400425 if err := s.ug.Encode(w, user); err != nil {
426 http.Error(w, err.Error(), http.StatusInternalServerError)
427 return
gio81246f02024-07-10 12:02:15 +0400428 }
429 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
430}
431
giob4a3a192024-08-19 09:55:47 +0400432type navItem struct {
433 Name string
434 Address string
435}
436
gio23bdc1b2024-07-11 16:07:47 +0400437type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400438 Navigation []navItem
439 Apps []string
440 Networks []installer.Network
441 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400442}
443
gio59946282024-10-07 12:55:51 +0400444func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400445 user := r.Context().Value(userCtx)
446 if user == nil {
447 http.Error(w, "unauthorized", http.StatusUnauthorized)
448 return
449 }
450 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400451 if err != nil {
452 http.Error(w, err.Error(), http.StatusInternalServerError)
453 return
454 }
gio11617ac2024-07-15 16:09:04 +0400455 networks, err := s.getNetworks(user.(string))
456 if err != nil {
457 http.Error(w, err.Error(), http.StatusInternalServerError)
458 return
459 }
giob54db242024-07-30 18:49:33 +0400460 var types []string
461 for _, t := range s.appTmpls.Types() {
462 types = append(types, strings.Replace(t, "-", ":", 1))
463 }
giob4a3a192024-08-19 09:55:47 +0400464 n := []navItem{navItem{"Home", "/"}}
465 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400466 if err := s.tmplts.index.Execute(w, data); err != nil {
467 http.Error(w, err.Error(), http.StatusInternalServerError)
468 return
gioa60f0de2024-07-08 10:49:48 +0400469 }
470}
471
gio5e49bb62024-07-20 10:43:19 +0400472type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400473 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400474 Name string
gio5887caa2024-10-03 15:07:23 +0400475 Branch string
gio5e49bb62024-07-20 10:43:19 +0400476 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400477 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400478 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400479 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400480}
481
gio59946282024-10-07 12:55:51 +0400482func (s *Server) handleAppStatus(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400483 vars := mux.Vars(r)
484 appName, ok := vars["app-name"]
485 if !ok || appName == "" {
486 http.Error(w, "missing app-name", http.StatusBadRequest)
487 return
488 }
gio7fbd4ad2024-08-27 10:06:39 +0400489 branch, ok := vars["branch"]
490 if !ok || branch == "" {
491 branch = "master"
492 }
gio94904702024-07-26 16:58:34 +0400493 u := r.Context().Value(userCtx)
494 if u == nil {
495 http.Error(w, "unauthorized", http.StatusUnauthorized)
496 return
497 }
498 user, ok := u.(string)
499 if !ok {
500 http.Error(w, "could not get user", http.StatusInternalServerError)
501 return
502 }
503 owner, err := s.st.GetAppOwner(appName)
504 if err != nil {
505 http.Error(w, err.Error(), http.StatusInternalServerError)
506 return
507 }
508 if owner != user {
509 http.Error(w, "unauthorized", http.StatusUnauthorized)
510 return
511 }
gio7fbd4ad2024-08-27 10:06:39 +0400512 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400513 if err != nil {
514 http.Error(w, err.Error(), http.StatusInternalServerError)
515 return
516 }
gio183e8342024-08-20 06:01:24 +0400517 var lastCommitResources resourceData
518 if len(commits) > 0 {
519 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
520 if err != nil {
521 http.Error(w, err.Error(), http.StatusInternalServerError)
522 return
523 }
524 r, err := extractResourceData(lastCommit.Resources.Helm)
525 if err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
528 }
529 lastCommitResources = r
530 }
gio7fbd4ad2024-08-27 10:06:39 +0400531 branches, err := s.st.GetBranches(appName)
532 if err != nil {
533 http.Error(w, err.Error(), http.StatusInternalServerError)
534 return
535 }
gio5e49bb62024-07-20 10:43:19 +0400536 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400537 Navigation: []navItem{
538 navItem{"Home", "/"},
539 navItem{appName, "/" + appName},
540 },
gio5e49bb62024-07-20 10:43:19 +0400541 Name: appName,
gio5887caa2024-10-03 15:07:23 +0400542 Branch: branch,
gio5e49bb62024-07-20 10:43:19 +0400543 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
544 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400545 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400546 Branches: branches,
547 }
548 if branch != "master" {
549 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400550 }
551 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
552 http.Error(w, err.Error(), http.StatusInternalServerError)
553 return
gioa60f0de2024-07-08 10:49:48 +0400554 }
gio0eaf2712024-04-14 13:08:46 +0400555}
556
giocfb228c2024-09-06 15:44:31 +0400557type appEnv struct {
558 Profile string `json:"envProfile"`
559}
560
gio59946282024-10-07 12:55:51 +0400561func (s *Server) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
giocfb228c2024-09-06 15:44:31 +0400562 vars := mux.Vars(r)
563 appName, ok := vars["app-name"]
564 if !ok || appName == "" {
565 http.Error(w, "missing app-name", http.StatusBadRequest)
566 return
567 }
568 branch, ok := vars["branch"]
569 if !ok || branch == "" {
570 branch = "master"
571 }
572 info, err := s.st.GetLastCommitInfo(appName, branch)
573 if err != nil {
574 http.Error(w, err.Error(), http.StatusInternalServerError)
575 return
576 }
577 var e appEnv
578 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
579 http.Error(w, err.Error(), http.StatusInternalServerError)
580 return
581 }
582 fmt.Fprintln(w, e.Profile)
583}
584
giob4a3a192024-08-19 09:55:47 +0400585type volume struct {
586 Name string
587 Size string
588}
589
590type postgresql struct {
591 Name string
592 Version string
593 Volume string
594}
595
596type ingress struct {
597 Host string
598}
599
gio7fbd4ad2024-08-27 10:06:39 +0400600type vm struct {
601 Name string
602 User string
603 CPUCores int
604 Memory string
605}
606
giob4a3a192024-08-19 09:55:47 +0400607type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400608 Volume []volume
609 PostgreSQL []postgresql
610 Ingress []ingress
611 VirtualMachine []vm
giob4a3a192024-08-19 09:55:47 +0400612}
613
614type commitStatusData struct {
615 Navigation []navItem
616 AppName string
617 Commit Commit
618 Resources resourceData
619}
620
gio59946282024-10-07 12:55:51 +0400621func (s *Server) handleAppCommit(w http.ResponseWriter, r *http.Request) {
giob4a3a192024-08-19 09:55:47 +0400622 vars := mux.Vars(r)
623 appName, ok := vars["app-name"]
624 if !ok || appName == "" {
625 http.Error(w, "missing app-name", http.StatusBadRequest)
626 return
627 }
628 hash, ok := vars["hash"]
629 if !ok || appName == "" {
630 http.Error(w, "missing app-name", http.StatusBadRequest)
631 return
632 }
633 u := r.Context().Value(userCtx)
634 if u == nil {
635 http.Error(w, "unauthorized", http.StatusUnauthorized)
636 return
637 }
638 user, ok := u.(string)
639 if !ok {
640 http.Error(w, "could not get user", http.StatusInternalServerError)
641 return
642 }
643 owner, err := s.st.GetAppOwner(appName)
644 if err != nil {
645 http.Error(w, err.Error(), http.StatusInternalServerError)
646 return
647 }
648 if owner != user {
649 http.Error(w, "unauthorized", http.StatusUnauthorized)
650 return
651 }
652 commit, err := s.st.GetCommit(hash)
653 if err != nil {
654 // TODO(gio): not-found ?
655 http.Error(w, err.Error(), http.StatusInternalServerError)
656 return
657 }
658 var res strings.Builder
659 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
660 http.Error(w, err.Error(), http.StatusInternalServerError)
661 return
662 }
663 resData, err := extractResourceData(commit.Resources.Helm)
664 if err != nil {
665 http.Error(w, err.Error(), http.StatusInternalServerError)
666 return
667 }
668 data := commitStatusData{
669 Navigation: []navItem{
670 navItem{"Home", "/"},
671 navItem{appName, "/" + appName},
672 navItem{hash, "/" + appName + "/" + hash},
673 },
674 AppName: appName,
675 Commit: commit,
676 Resources: resData,
677 }
678 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
679 http.Error(w, err.Error(), http.StatusInternalServerError)
680 return
681 }
682}
683
gio183e8342024-08-20 06:01:24 +0400684type logData struct {
685 Navigation []navItem
686 AppName string
687 Logs template.HTML
688}
689
gio59946282024-10-07 12:55:51 +0400690func (s *Server) handleAppLogs(w http.ResponseWriter, r *http.Request) {
gio183e8342024-08-20 06:01:24 +0400691 vars := mux.Vars(r)
692 appName, ok := vars["app-name"]
693 if !ok || appName == "" {
694 http.Error(w, "missing app-name", http.StatusBadRequest)
695 return
696 }
697 u := r.Context().Value(userCtx)
698 if u == nil {
699 http.Error(w, "unauthorized", http.StatusUnauthorized)
700 return
701 }
702 user, ok := u.(string)
703 if !ok {
704 http.Error(w, "could not get user", http.StatusInternalServerError)
705 return
706 }
707 owner, err := s.st.GetAppOwner(appName)
708 if err != nil {
709 http.Error(w, err.Error(), http.StatusInternalServerError)
710 return
711 }
712 if owner != user {
713 http.Error(w, "unauthorized", http.StatusUnauthorized)
714 return
715 }
716 data := logData{
717 Navigation: []navItem{
718 navItem{"Home", "/"},
719 navItem{appName, "/" + appName},
720 navItem{"Logs", "/" + appName + "/logs"},
721 },
722 AppName: appName,
723 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
724 }
725 if err := s.tmplts.logs.Execute(w, data); err != nil {
726 fmt.Println(err)
727 http.Error(w, err.Error(), http.StatusInternalServerError)
728 return
729 }
730}
731
gio81246f02024-07-10 12:02:15 +0400732type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400733 Ref string `json:"ref"`
734 Repository struct {
735 Name string `json:"name"`
736 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400737 After string `json:"after"`
738 Commits []struct {
739 Id string `json:"id"`
740 Message string `json:"message"`
741 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400742}
743
gio59946282024-10-07 12:55:51 +0400744func (s *Server) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400745 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400746 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400747 var contents strings.Builder
748 io.Copy(&contents, r.Body)
749 c := contents.String()
750 fmt.Println(c)
751 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400752 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400753 return
754 }
gio7fbd4ad2024-08-27 10:06:39 +0400755 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
756 return
757 }
758 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
759 if !ok {
760 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400761 return
762 }
gioa60f0de2024-07-08 10:49:48 +0400763 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400764 go func() {
gio11617ac2024-07-15 16:09:04 +0400765 owner, err := s.st.GetAppOwner(req.Repository.Name)
766 if err != nil {
767 return
768 }
769 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400770 if err != nil {
771 return
772 }
giof15b9da2024-09-19 06:59:16 +0400773 // TODO(gio): get only available ones by owner
774 clusters, err := s.getClusters()
775 if err != nil {
776 return
777 }
gio94904702024-07-26 16:58:34 +0400778 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
779 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
780 if err != nil {
781 return
782 }
gioe2e31e12024-08-18 08:20:56 +0400783 found := false
784 commitMsg := ""
785 for _, c := range req.Commits {
786 if c.Id == req.After {
787 found = true
788 commitMsg = c.Message
789 break
gioa60f0de2024-07-08 10:49:48 +0400790 }
791 }
gioe2e31e12024-08-18 08:20:56 +0400792 if !found {
793 fmt.Printf("Error: could not find commit message")
794 return
795 }
gio7fbd4ad2024-08-27 10:06:39 +0400796 s.l.Lock()
797 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400798 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 +0400799 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400800 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400801 return
802 }
gioa60f0de2024-07-08 10:49:48 +0400803 for addr, _ := range s.workers[req.Repository.Name] {
804 go func() {
805 // TODO(gio): make port configurable
806 http.Get(fmt.Sprintf("http://%s/update", addr))
807 }()
gio0eaf2712024-04-14 13:08:46 +0400808 }
809 }()
gio0eaf2712024-04-14 13:08:46 +0400810}
811
gio81246f02024-07-10 12:02:15 +0400812type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400813 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400814 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400815}
816
gio59946282024-10-07 12:55:51 +0400817func (s *Server) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400818 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400819 vars := mux.Vars(r)
820 appName, ok := vars["app-name"]
821 if !ok || appName == "" {
822 http.Error(w, "missing app-name", http.StatusBadRequest)
823 return
824 }
gio81246f02024-07-10 12:02:15 +0400825 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400826 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
827 http.Error(w, err.Error(), http.StatusInternalServerError)
828 return
829 }
gioa60f0de2024-07-08 10:49:48 +0400830 if _, ok := s.workers[appName]; !ok {
831 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400832 }
gioa60f0de2024-07-08 10:49:48 +0400833 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400834 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400835}
836
gio59946282024-10-07 12:55:51 +0400837func (s *Server) handleCreateApp(w http.ResponseWriter, r *http.Request) {
gio11617ac2024-07-15 16:09:04 +0400838 u := r.Context().Value(userCtx)
839 if u == nil {
840 http.Error(w, "unauthorized", http.StatusUnauthorized)
841 return
842 }
843 user, ok := u.(string)
844 if !ok {
845 http.Error(w, "could not get user", http.StatusInternalServerError)
846 return
847 }
848 network := r.FormValue("network")
849 if network == "" {
850 http.Error(w, "missing network", http.StatusBadRequest)
851 return
852 }
gio5e49bb62024-07-20 10:43:19 +0400853 subdomain := r.FormValue("subdomain")
854 if subdomain == "" {
855 http.Error(w, "missing subdomain", http.StatusBadRequest)
856 return
857 }
858 appType := r.FormValue("type")
859 if appType == "" {
860 http.Error(w, "missing type", http.StatusBadRequest)
861 return
862 }
gio5cc6afc2024-10-06 09:33:44 +0400863 appName := r.FormValue("name")
864 var err error
865 if appName == "" {
866 g := installer.NewFixedLengthRandomNameGenerator(3)
867 appName, err = g.Generate()
868 }
gio11617ac2024-07-15 16:09:04 +0400869 if err != nil {
870 http.Error(w, err.Error(), http.StatusInternalServerError)
871 return
872 }
873 if ok, err := s.client.UserExists(user); err != nil {
874 http.Error(w, err.Error(), http.StatusInternalServerError)
875 return
876 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400877 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
878 return
gio11617ac2024-07-15 16:09:04 +0400879 }
giocafd4e62024-07-31 10:53:40 +0400880 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400881 http.Error(w, err.Error(), http.StatusInternalServerError)
882 return
883 }
884 if err := s.st.CreateApp(appName, user); err != nil {
885 http.Error(w, err.Error(), http.StatusInternalServerError)
886 return
887 }
giod8ab4f52024-07-26 16:58:34 +0400888 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400889 http.Error(w, err.Error(), http.StatusInternalServerError)
890 return
891 }
892 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
893}
894
gio59946282024-10-07 12:55:51 +0400895func (s *Server) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400896 u := r.Context().Value(userCtx)
897 if u == nil {
898 http.Error(w, "unauthorized", http.StatusUnauthorized)
899 return
900 }
901 user, ok := u.(string)
902 if !ok {
903 http.Error(w, "could not get user", http.StatusInternalServerError)
904 return
905 }
906 vars := mux.Vars(r)
907 appName, ok := vars["app-name"]
908 if !ok || appName == "" {
909 http.Error(w, "missing app-name", http.StatusBadRequest)
910 return
911 }
912 branch := r.FormValue("branch")
913 if branch == "" {
gio5887caa2024-10-03 15:07:23 +0400914 http.Error(w, "missing branch", http.StatusBadRequest)
gio7fbd4ad2024-08-27 10:06:39 +0400915 return
916 }
917 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
918 http.Error(w, err.Error(), http.StatusInternalServerError)
919 return
920 }
921 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
922}
923
gio59946282024-10-07 12:55:51 +0400924func (s *Server) handleBranchDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +0400925 u := r.Context().Value(userCtx)
926 if u == nil {
927 http.Error(w, "unauthorized", http.StatusUnauthorized)
928 return
929 }
930 vars := mux.Vars(r)
931 appName, ok := vars["app-name"]
932 if !ok || appName == "" {
933 http.Error(w, "missing app-name", http.StatusBadRequest)
934 return
935 }
936 branch, ok := vars["branch"]
937 if !ok || branch == "" {
938 http.Error(w, "missing branch", http.StatusBadRequest)
939 return
940 }
941 if err := s.deleteBranch(appName, branch); err != nil {
942 http.Error(w, err.Error(), http.StatusInternalServerError)
943 return
944 }
945 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
946}
947
gio59946282024-10-07 12:55:51 +0400948func (s *Server) handleAppDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +0400949 u := r.Context().Value(userCtx)
950 if u == nil {
951 http.Error(w, "unauthorized", http.StatusUnauthorized)
952 return
953 }
954 vars := mux.Vars(r)
955 appName, ok := vars["app-name"]
956 if !ok || appName == "" {
957 http.Error(w, "missing app-name", http.StatusBadRequest)
958 return
959 }
960 if err := s.deleteApp(appName); err != nil {
961 http.Error(w, err.Error(), http.StatusInternalServerError)
962 return
963 }
gioe44c1512024-10-06 14:13:55 +0400964 http.Redirect(w, r, "/", http.StatusSeeOther)
gio5887caa2024-10-03 15:07:23 +0400965}
966
gio81246f02024-07-10 12:02:15 +0400967type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400968 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400969 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400970 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400971 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400972}
973
gio81246f02024-07-10 12:02:15 +0400974type apiCreateAppResp struct {
975 AppName string `json:"appName"`
976 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400977}
978
gio59946282024-10-07 12:55:51 +0400979func (s *Server) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400980 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400981 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400982 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
983 http.Error(w, err.Error(), http.StatusBadRequest)
984 return
985 }
986 g := installer.NewFixedLengthRandomNameGenerator(3)
987 appName, err := g.Generate()
988 if err != nil {
989 http.Error(w, err.Error(), http.StatusInternalServerError)
990 return
991 }
gio11617ac2024-07-15 16:09:04 +0400992 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400993 if err != nil {
gio33059762024-07-05 13:19:07 +0400994 http.Error(w, err.Error(), http.StatusInternalServerError)
995 return
996 }
gio11617ac2024-07-15 16:09:04 +0400997 if user != "" {
998 http.Error(w, "public key already registered", http.StatusBadRequest)
999 return
1000 }
1001 user = appName
1002 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
1003 http.Error(w, err.Error(), http.StatusInternalServerError)
1004 return
1005 }
1006 password := generatePassword()
1007 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1008 if err != nil {
1009 http.Error(w, err.Error(), http.StatusInternalServerError)
1010 return
1011 }
giocafd4e62024-07-31 10:53:40 +04001012 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +04001013 http.Error(w, err.Error(), http.StatusInternalServerError)
1014 return
1015 }
1016 if err := s.st.CreateApp(appName, user); err != nil {
1017 http.Error(w, err.Error(), http.StatusInternalServerError)
1018 return
1019 }
giod8ab4f52024-07-26 16:58:34 +04001020 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +04001021 http.Error(w, err.Error(), http.StatusInternalServerError)
1022 return
1023 }
gio81246f02024-07-10 12:02:15 +04001024 resp := apiCreateAppResp{
1025 AppName: appName,
1026 Password: password,
1027 }
gio33059762024-07-05 13:19:07 +04001028 if err := json.NewEncoder(w).Encode(resp); err != nil {
1029 http.Error(w, err.Error(), http.StatusInternalServerError)
1030 return
1031 }
1032}
1033
gio59946282024-10-07 12:55:51 +04001034func (s *Server) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +04001035 if !s.external {
giod8ab4f52024-07-26 16:58:34 +04001036 return true
1037 }
1038 for _, cfg := range s.appConfigs {
1039 if strings.ToLower(cfg.Network) == network {
1040 return false
1041 }
1042 }
1043 return true
1044}
1045
gio59946282024-10-07 12:55:51 +04001046func (s *Server) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +04001047 s.l.Lock()
1048 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +04001049 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +04001050 network = strings.ToLower(network)
1051 if !s.isNetworkUseAllowed(network) {
1052 return fmt.Errorf("network already used: %s", network)
1053 }
gio33059762024-07-05 13:19:07 +04001054 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001055 return err
gio33059762024-07-05 13:19:07 +04001056 } else if ok {
gio11617ac2024-07-15 16:09:04 +04001057 return nil
gioa60f0de2024-07-08 10:49:48 +04001058 }
gio5e49bb62024-07-20 10:43:19 +04001059 networks, err := s.getNetworks(user)
1060 if err != nil {
1061 return err
1062 }
giod8ab4f52024-07-26 16:58:34 +04001063 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +04001064 if !ok {
1065 return fmt.Errorf("network not found: %s\n", network)
1066 }
gio33059762024-07-05 13:19:07 +04001067 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001068 return err
gio33059762024-07-05 13:19:07 +04001069 }
1070 appRepo, err := s.client.GetRepo(appName)
1071 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001072 return err
gio33059762024-07-05 13:19:07 +04001073 }
gio7fbd4ad2024-08-27 10:06:39 +04001074 files, err := s.renderAppConfigTemplate(appType, n, subdomain)
1075 if err != nil {
1076 return err
1077 }
1078 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1079}
1080
gio59946282024-10-07 12:55:51 +04001081func (s *Server) createDevBranch(appName, fromBranch, toBranch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001082 s.l.Lock()
1083 defer s.l.Unlock()
1084 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1085 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1086 if err != nil {
1087 return err
1088 }
gioc81a8472024-09-24 13:06:19 +02001089 appCfg, err := soft.ReadFile(appRepo, "app.json")
gio7fbd4ad2024-08-27 10:06:39 +04001090 if err != nil {
1091 return err
1092 }
1093 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user)
1094 if err != nil {
1095 return err
1096 }
gioc81a8472024-09-24 13:06:19 +02001097 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
gio7fbd4ad2024-08-27 10:06:39 +04001098}
1099
gio59946282024-10-07 12:55:51 +04001100func (s *Server) deleteBranch(appName string, branch string) error {
gio5887caa2024-10-03 15:07:23 +04001101 appBranch := fmt.Sprintf("dodo_%s", branch)
1102 hf := installer.NewGitHelmFetcher()
1103 if err := func() error {
1104 repo, err := s.client.GetRepoBranch(appName, appBranch)
1105 if err != nil {
1106 return err
1107 }
1108 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
1109 if err != nil {
1110 return err
1111 }
1112 return m.Remove("app")
1113 }(); err != nil {
1114 return err
1115 }
1116 configRepo, err := s.client.GetRepo(ConfigRepoName)
1117 if err != nil {
1118 return err
1119 }
1120 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
1121 if err != nil {
1122 return err
1123 }
1124 appPath := fmt.Sprintf("%s/%s", appName, branch)
gio829b1b72024-10-05 21:50:56 +04001125 if err := m.Remove(appPath); err != nil {
gio5887caa2024-10-03 15:07:23 +04001126 return err
1127 }
1128 if err := s.client.DeleteRepoBranch(appName, appBranch); err != nil {
1129 return err
1130 }
1131 if branch != "master" {
gioe44c1512024-10-06 14:13:55 +04001132 if err := s.client.DeleteRepoBranch(appName, branch); err != nil {
1133 return err
1134 }
gio5887caa2024-10-03 15:07:23 +04001135 }
gioe44c1512024-10-06 14:13:55 +04001136 return s.st.DeleteBranch(appName, branch)
gio5887caa2024-10-03 15:07:23 +04001137}
1138
gio59946282024-10-07 12:55:51 +04001139func (s *Server) deleteApp(appName string) error {
gio5887caa2024-10-03 15:07:23 +04001140 configRepo, err := s.client.GetRepo(ConfigRepoName)
1141 if err != nil {
1142 return err
1143 }
1144 branches, err := configRepo.ListDir(fmt.Sprintf("/%s", appName))
1145 if err != nil {
1146 return err
1147 }
1148 for _, b := range branches {
1149 if !b.IsDir() || strings.HasPrefix(b.Name(), "dodo_") {
1150 continue
1151 }
1152 if err := s.deleteBranch(appName, b.Name()); err != nil {
1153 return err
1154 }
1155 }
gioe44c1512024-10-06 14:13:55 +04001156 if err := s.client.DeleteRepo(appName); err != nil {
1157 return err
1158 }
1159 return s.st.DeleteApp(appName)
gio5887caa2024-10-03 15:07:23 +04001160}
1161
gio59946282024-10-07 12:55:51 +04001162func (s *Server) createAppForBranch(
gio7fbd4ad2024-08-27 10:06:39 +04001163 repo soft.RepoIO,
1164 appName string,
1165 branch string,
1166 user string,
1167 network string,
1168 files map[string][]byte,
1169) error {
1170 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1171 for path, contents := range files {
1172 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1173 return "", err
1174 }
1175 }
1176 return "init", nil
1177 }, soft.WithCommitToBranch(branch))
1178 if err != nil {
1179 return err
1180 }
1181 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001182 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001183 return err
gio33059762024-07-05 13:19:07 +04001184 }
giof15b9da2024-09-19 06:59:16 +04001185 // TODO(gio): get only available ones by owner
1186 clusters, err := s.getClusters()
1187 if err != nil {
1188 return err
1189 }
gio33059762024-07-05 13:19:07 +04001190 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001191 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1192 if err != nil {
1193 return err
1194 }
1195 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001196 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001197 return err
gio33059762024-07-05 13:19:07 +04001198 }
1199 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1200 suffix, err := suffixGen.Generate()
1201 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001202 return err
gio33059762024-07-05 13:19:07 +04001203 }
gio94904702024-07-26 16:58:34 +04001204 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001205 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001206 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001207 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001208 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001209 return err
1210 }
gio7fbd4ad2024-08-27 10:06:39 +04001211 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001212 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001213 return err
gio33059762024-07-05 13:19:07 +04001214 }
giod8ab4f52024-07-26 16:58:34 +04001215 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001216 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001217 return err
gio33059762024-07-05 13:19:07 +04001218 }
1219 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001220 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001221 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001222 return err
gio33059762024-07-05 13:19:07 +04001223 }
gio7fbd4ad2024-08-27 10:06:39 +04001224 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001225 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001226 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001227 if err != nil {
1228 return "", err
1229 }
1230 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001231 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001232 return "", err
1233 }
1234 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001235 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001236 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001237 appPath,
gio9d66f322024-07-06 13:45:10 +04001238 namespace,
1239 map[string]any{
1240 "repoAddr": s.client.GetRepoAddress(appName),
1241 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001242 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001243 "gitRepoPublicKey": s.gitRepoPublicKey,
1244 },
1245 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001246 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001247 installer.WithNoPublish(),
1248 installer.WithNoLock(),
1249 ); err != nil {
1250 return "", err
1251 }
1252 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001253 })
1254 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001255 return err
gio33059762024-07-05 13:19:07 +04001256 }
gio7fbd4ad2024-08-27 10:06:39 +04001257 return s.initAppACLs(m, appPath, appName, branch, user)
1258}
1259
gio59946282024-10-07 12:55:51 +04001260func (s *Server) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001261 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001262 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001263 return err
gio33059762024-07-05 13:19:07 +04001264 }
1265 fluxKeys, ok := cfg.Input["fluxKeys"]
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 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1270 if !ok {
gio11617ac2024-07-15 16:09:04 +04001271 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001272 }
1273 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001274 return err
gio33059762024-07-05 13:19:07 +04001275 } else if ok {
1276 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001277 return err
gio33059762024-07-05 13:19:07 +04001278 }
1279 } else {
1280 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001281 return err
gio33059762024-07-05 13:19:07 +04001282 }
1283 }
gio7fbd4ad2024-08-27 10:06:39 +04001284 if branch != "master" {
1285 return nil
1286 }
gio33059762024-07-05 13:19:07 +04001287 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001288 return err
gio33059762024-07-05 13:19:07 +04001289 }
gio7fbd4ad2024-08-27 10:06:39 +04001290 if err := s.client.AddReadWriteCollaborator(appName, user); 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.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001294 return err
gio33059762024-07-05 13:19:07 +04001295 }
gio2ccb6e32024-08-15 12:01:33 +04001296 if !s.external {
1297 go func() {
1298 users, err := s.client.GetAllUsers()
1299 if err != nil {
1300 fmt.Println(err)
1301 return
1302 }
1303 for _, user := range users {
1304 // TODO(gio): fluxcd should have only read access
1305 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1306 fmt.Println(err)
1307 }
1308 }
1309 }()
1310 }
gio43b0f422024-08-21 10:40:13 +04001311 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1312 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001313 return nil
gio33059762024-07-05 13:19:07 +04001314}
1315
gio81246f02024-07-10 12:02:15 +04001316type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001317 User string `json:"user"`
1318 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001319}
1320
gio59946282024-10-07 12:55:51 +04001321func (s *Server) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001322 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001323 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1324 http.Error(w, err.Error(), http.StatusBadRequest)
1325 return
1326 }
gio7fbd4ad2024-08-27 10:06:39 +04001327 if req.User == "" {
1328 http.Error(w, "invalid user", http.StatusBadRequest)
1329 return
1330 }
1331 if req.PublicKey == "" {
1332 http.Error(w, "invalid public key", http.StatusBadRequest)
1333 return
1334 }
1335 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001336 http.Error(w, err.Error(), http.StatusInternalServerError)
1337 return
1338 }
1339}
1340
gio94904702024-07-26 16:58:34 +04001341type dodoAppRendered struct {
1342 App struct {
1343 Ingress struct {
1344 Network string `json:"network"`
1345 Subdomain string `json:"subdomain"`
1346 } `json:"ingress"`
1347 } `json:"app"`
1348 Input struct {
1349 AppId string `json:"appId"`
1350 } `json:"input"`
1351}
1352
gio7fbd4ad2024-08-27 10:06:39 +04001353// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio59946282024-10-07 12:55:51 +04001354func (s *Server) updateDodoApp(
gio43b0f422024-08-21 10:40:13 +04001355 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001356 name string,
1357 branch string,
1358 namespace string,
gio43b0f422024-08-21 10:40:13 +04001359 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001360 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001361 owner string,
gio43b0f422024-08-21 10:40:13 +04001362) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001363 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001364 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001365 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001366 }
giof8843412024-05-22 16:38:05 +04001367 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001368 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
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 }
gioc81a8472024-09-24 13:06:19 +02001372 appCfg, err := soft.ReadFile(repo, "app.json")
gio0eaf2712024-04-14 13:08:46 +04001373 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001374 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001375 }
1376 app, err := installer.NewDodoApp(appCfg)
1377 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001378 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001379 }
giof8843412024-05-22 16:38:05 +04001380 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001381 var ret installer.ReleaseResources
1382 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1383 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001384 app,
1385 "app",
1386 "/.dodo/app",
1387 namespace,
1388 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001389 "repoAddr": repo.FullAddress(),
1390 "repoPublicAddr": s.repoPublicAddr,
1391 "managerAddr": fmt.Sprintf("http://%s", s.self),
1392 "appId": name,
1393 "branch": branch,
1394 "sshPrivateKey": s.sshKey,
1395 "username": owner,
gio94904702024-07-26 16:58:34 +04001396 },
1397 installer.WithNoPull(),
1398 installer.WithNoPublish(),
1399 installer.WithConfig(&s.env),
1400 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001401 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001402 installer.WithLocalChartGenerator(lg),
1403 installer.WithNoLock(),
1404 )
1405 if err != nil {
1406 return "", err
1407 }
1408 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001409 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001410 return "", nil
1411 }
1412 if _, err := m.Install(
1413 appStatus,
1414 "status",
1415 "/.dodo/status",
1416 s.namespace,
1417 map[string]any{
1418 "appName": rendered.Input.AppId,
1419 "network": rendered.App.Ingress.Network,
1420 "appSubdomain": rendered.App.Ingress.Subdomain,
1421 },
1422 installer.WithNoPull(),
1423 installer.WithNoPublish(),
1424 installer.WithConfig(&s.env),
1425 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001426 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001427 installer.WithLocalChartGenerator(lg),
1428 installer.WithNoLock(),
1429 ); err != nil {
1430 return "", err
1431 }
1432 return "install app", nil
1433 },
gio7fbd4ad2024-08-27 10:06:39 +04001434 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001435 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001436 ); err != nil {
1437 return installer.ReleaseResources{}, err
1438 }
gio43b0f422024-08-21 10:40:13 +04001439 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1440 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001441 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001442}
gio33059762024-07-05 13:19:07 +04001443
gio59946282024-10-07 12:55:51 +04001444func (s *Server) renderAppConfigTemplate(appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001445 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001446 appTmpl, err := s.appTmpls.Find(appType)
1447 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001448 return nil, err
gio33059762024-07-05 13:19:07 +04001449 }
giod99b2bd2024-10-09 18:29:15 +04001450 return appTmpl.Render(fmt.Sprintf("%s/schemas/app.schema.json", s.selfPublic), network, subdomain)
gio33059762024-07-05 13:19:07 +04001451}
gio81246f02024-07-10 12:02:15 +04001452
1453func generatePassword() string {
1454 return "foo"
1455}
giocb34ad22024-07-11 08:01:13 +04001456
gio59946282024-10-07 12:55:51 +04001457func (s *Server) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001458 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001459 resp, err := http.Get(addr)
1460 if err != nil {
1461 return nil, err
1462 }
gio23bdc1b2024-07-11 16:07:47 +04001463 networks := []installer.Network{}
1464 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001465 return nil, err
1466 }
gio11617ac2024-07-15 16:09:04 +04001467 return s.nf.Filter(user, networks)
1468}
1469
gio59946282024-10-07 12:55:51 +04001470func (s *Server) getClusters() ([]installer.Cluster, error) {
giof15b9da2024-09-19 06:59:16 +04001471 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1472 resp, err := http.Get(addr)
1473 if err != nil {
1474 return nil, err
1475 }
1476 clusters := []installer.Cluster{}
1477 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1478 return nil, err
1479 }
1480 fmt.Printf("CLUSTERS %+v\n", clusters)
1481 return clusters, nil
1482}
1483
gio8fae3af2024-07-25 13:43:31 +04001484type publicNetworkData struct {
1485 Name string `json:"name"`
1486 Domain string `json:"domain"`
1487}
1488
1489type publicData struct {
1490 Networks []publicNetworkData `json:"networks"`
1491 Types []string `json:"types"`
1492}
1493
gio59946282024-10-07 12:55:51 +04001494func (s *Server) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001495 w.Header().Set("Access-Control-Allow-Origin", "*")
1496 s.l.Lock()
1497 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001498 networks, err := s.getNetworks("")
1499 if err != nil {
1500 http.Error(w, err.Error(), http.StatusInternalServerError)
1501 return
1502 }
1503 var ret publicData
1504 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001505 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1506 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1507 }
gio8fae3af2024-07-25 13:43:31 +04001508 }
1509 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001510 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001511 }
gio8fae3af2024-07-25 13:43:31 +04001512 if err := json.NewEncoder(w).Encode(ret); err != nil {
1513 http.Error(w, err.Error(), http.StatusInternalServerError)
1514 return
1515 }
1516}
1517
gio59946282024-10-07 12:55:51 +04001518func (s *Server) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001519 if err != nil {
1520 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001521 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001522 fmt.Printf("Error: %s\n", err.Error())
1523 return err
1524 }
1525 return err
1526 }
1527 var resB bytes.Buffer
1528 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001529 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001530 fmt.Printf("Error: %s\n", err.Error())
1531 return err
1532 }
1533 return err
1534 }
gio7fbd4ad2024-08-27 10:06:39 +04001535 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001536 fmt.Printf("Error: %s\n", err.Error())
1537 return err
1538 }
1539 return nil
1540}
1541
gio11617ac2024-07-15 16:09:04 +04001542func pickNetwork(networks []installer.Network, network string) []installer.Network {
1543 for _, n := range networks {
1544 if n.Name == network {
1545 return []installer.Network{n}
1546 }
1547 }
1548 return []installer.Network{}
1549}
1550
1551type NetworkFilter interface {
1552 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1553}
1554
1555type noNetworkFilter struct{}
1556
1557func NewNoNetworkFilter() NetworkFilter {
1558 return noNetworkFilter{}
1559}
1560
gio8fae3af2024-07-25 13:43:31 +04001561func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001562 return networks, nil
1563}
1564
1565type filterByOwner struct {
1566 st Store
1567}
1568
1569func NewNetworkFilterByOwner(st Store) NetworkFilter {
1570 return &filterByOwner{st}
1571}
1572
1573func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001574 if user == "" {
1575 return networks, nil
1576 }
gio11617ac2024-07-15 16:09:04 +04001577 network, err := f.st.GetUserNetwork(user)
1578 if err != nil {
1579 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001580 }
1581 ret := []installer.Network{}
1582 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001583 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001584 ret = append(ret, n)
1585 }
1586 }
giocb34ad22024-07-11 08:01:13 +04001587 return ret, nil
1588}
gio11617ac2024-07-15 16:09:04 +04001589
1590type allowListFilter struct {
1591 allowed []string
1592}
1593
1594func NewAllowListFilter(allowed []string) NetworkFilter {
1595 return &allowListFilter{allowed}
1596}
1597
gio8fae3af2024-07-25 13:43:31 +04001598func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001599 ret := []installer.Network{}
1600 for _, n := range networks {
1601 if slices.Contains(f.allowed, n.Name) {
1602 ret = append(ret, n)
1603 }
1604 }
1605 return ret, nil
1606}
1607
1608type combinedNetworkFilter struct {
1609 filters []NetworkFilter
1610}
1611
1612func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1613 return &combinedNetworkFilter{filters}
1614}
1615
gio8fae3af2024-07-25 13:43:31 +04001616func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001617 ret := networks
1618 var err error
1619 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001620 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001621 if err != nil {
1622 return nil, err
1623 }
1624 }
1625 return ret, nil
1626}
giocafd4e62024-07-31 10:53:40 +04001627
1628type user struct {
1629 Username string `json:"username"`
1630 Email string `json:"email"`
1631 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1632}
1633
gio59946282024-10-07 12:55:51 +04001634func (s *Server) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
giocafd4e62024-07-31 10:53:40 +04001635 go s.syncUsers()
1636}
1637
gio59946282024-10-07 12:55:51 +04001638func (s *Server) syncUsers() {
giocafd4e62024-07-31 10:53:40 +04001639 if s.external {
1640 panic("MUST NOT REACH!")
1641 }
1642 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1643 if err != nil {
1644 return
1645 }
1646 users := []user{}
1647 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1648 fmt.Println(err)
1649 return
1650 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001651 validUsernames := make(map[string]user)
1652 for _, u := range users {
1653 validUsernames[u.Username] = u
1654 }
1655 allClientUsers, err := s.client.GetAllUsers()
1656 if err != nil {
1657 fmt.Println(err)
1658 return
1659 }
1660 keyToUser := make(map[string]string)
1661 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001662 if clientUser == "admin" || clientUser == "fluxcd" {
1663 continue
1664 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001665 userData, ok := validUsernames[clientUser]
1666 if !ok {
1667 if err := s.client.RemoveUser(clientUser); err != nil {
1668 fmt.Println(err)
1669 return
1670 }
1671 } else {
1672 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1673 if err != nil {
1674 fmt.Println(err)
1675 return
1676 }
1677 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001678 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001679 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001680 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001681 })
1682 if !keyOk {
1683 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1684 fmt.Println(err)
1685 }
1686 } else {
1687 keyToUser[cleanKey] = clientUser
1688 }
1689 }
1690 }
1691 }
giocafd4e62024-07-31 10:53:40 +04001692 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001693 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1694 fmt.Println(err)
1695 return
1696 }
giocafd4e62024-07-31 10:53:40 +04001697 if len(u.SSHPublicKeys) == 0 {
1698 continue
1699 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001700 ok, err := s.client.UserExists(u.Username)
1701 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001702 fmt.Println(err)
1703 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001704 }
1705 if !ok {
1706 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1707 fmt.Println(err)
1708 return
1709 }
1710 } else {
1711 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001712 cleanKey := soft.CleanKey(key)
1713 if user, ok := keyToUser[cleanKey]; ok {
1714 if u.Username != user {
1715 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1716 }
1717 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001718 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001719 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001720 fmt.Println(err)
1721 return
giocafd4e62024-07-31 10:53:40 +04001722 }
1723 }
1724 }
1725 }
1726 repos, err := s.client.GetAllRepos()
1727 if err != nil {
1728 return
1729 }
1730 for _, r := range repos {
1731 if r == ConfigRepoName {
1732 continue
1733 }
1734 for _, u := range users {
1735 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1736 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001737 continue
giocafd4e62024-07-31 10:53:40 +04001738 }
1739 }
1740 }
1741}
giob4a3a192024-08-19 09:55:47 +04001742
1743func extractResourceData(resources []installer.Resource) (resourceData, error) {
1744 var ret resourceData
1745 for _, r := range resources {
1746 t, ok := r.Annotations["dodo.cloud/resource-type"]
1747 if !ok {
1748 continue
1749 }
1750 switch t {
1751 case "volume":
1752 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1753 if !ok {
1754 return resourceData{}, fmt.Errorf("no name")
1755 }
1756 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1757 if !ok {
1758 return resourceData{}, fmt.Errorf("no size")
1759 }
1760 ret.Volume = append(ret.Volume, volume{name, size})
1761 case "postgresql":
1762 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1763 if !ok {
1764 return resourceData{}, fmt.Errorf("no name")
1765 }
1766 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1767 if !ok {
1768 return resourceData{}, fmt.Errorf("no version")
1769 }
1770 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1771 if !ok {
1772 return resourceData{}, fmt.Errorf("no volume")
1773 }
1774 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
1775 case "ingress":
1776 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1777 if !ok {
1778 return resourceData{}, fmt.Errorf("no host")
1779 }
1780 ret.Ingress = append(ret.Ingress, ingress{host})
gio7fbd4ad2024-08-27 10:06:39 +04001781 case "virtual-machine":
1782 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1783 if !ok {
1784 return resourceData{}, fmt.Errorf("no name")
1785 }
1786 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1787 if !ok {
1788 return resourceData{}, fmt.Errorf("no user")
1789 }
1790 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1791 if !ok {
1792 return resourceData{}, fmt.Errorf("no cpu cores")
1793 }
1794 cpuCores, err := strconv.Atoi(cpuCoresS)
1795 if err != nil {
1796 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1797 }
1798 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1799 if !ok {
1800 return resourceData{}, fmt.Errorf("no memory")
1801 }
1802 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001803 default:
1804 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1805 }
1806 }
1807 return ret, nil
1808}
gio7fbd4ad2024-08-27 10:06:39 +04001809
1810func createDevBranchAppConfig(from []byte, branch, username string) (string, []byte, error) {
gioc81a8472024-09-24 13:06:19 +02001811 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
1812 "app.cue": from,
1813 })
gio7fbd4ad2024-08-27 10:06:39 +04001814 if err != nil {
1815 return "", nil, err
1816 }
1817 if err := cfg.Err(); err != nil {
1818 return "", nil, err
1819 }
1820 if err := cfg.Validate(); err != nil {
1821 return "", nil, err
1822 }
1823 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1824 if err := subdomain.Err(); err != nil {
1825 return "", nil, err
1826 }
1827 subdomainStr, err := subdomain.String()
1828 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1829 if err := network.Err(); err != nil {
1830 return "", nil, err
1831 }
1832 networkStr, err := network.String()
1833 if err != nil {
1834 return "", nil, err
1835 }
1836 newCfg := map[string]any{}
1837 if err := cfg.Decode(&newCfg); err != nil {
1838 return "", nil, err
1839 }
1840 app, ok := newCfg["app"].(map[string]any)
1841 if !ok {
1842 return "", nil, fmt.Errorf("not a map")
1843 }
1844 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1845 app["dev"] = map[string]any{
1846 "enabled": true,
1847 "username": username,
1848 }
1849 buf, err := json.MarshalIndent(newCfg, "", "\t")
1850 if err != nil {
1851 return "", nil, err
1852 }
1853 return networkStr, buf, nil
1854}