blob: 2bbbb7c18e47151e1820cf5a923ff3451ff9042f [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"
gio9f6b27d2024-10-14 10:08:40 +040019 ttemplate "text/template"
giocafd4e62024-07-31 10:53:40 +040020 "time"
gio0eaf2712024-04-14 13:08:46 +040021
Davit Tabidzea5ea5092024-08-01 15:28:09 +040022 "golang.org/x/crypto/bcrypt"
23 "golang.org/x/exp/rand"
24
gio0eaf2712024-04-14 13:08:46 +040025 "github.com/giolekva/pcloud/core/installer"
gio59946282024-10-07 12:55:51 +040026 "github.com/giolekva/pcloud/core/installer/server"
gio0eaf2712024-04-14 13:08:46 +040027 "github.com/giolekva/pcloud/core/installer/soft"
gio43b0f422024-08-21 10:40:13 +040028 "github.com/giolekva/pcloud/core/installer/tasks"
gio33059762024-07-05 13:19:07 +040029
gio7fbd4ad2024-08-27 10:06:39 +040030 "cuelang.org/go/cue"
gio33059762024-07-05 13:19:07 +040031 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040032 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040033)
34
gio59946282024-10-07 12:55:51 +040035//go:embed templates/*
36var templates embed.FS
gio23bdc1b2024-07-11 16:07:47 +040037
gio59946282024-10-07 12:55:51 +040038//go:embed all:app-templates
gio5e49bb62024-07-20 10:43:19 +040039var appTmplsFS embed.FS
40
gio59946282024-10-07 12:55:51 +040041//go:embed static/*
42var staticAssets embed.FS
43
gio9f6b27d2024-10-14 10:08:40 +040044//go:embed schemas/app.schema.json
45var dodoAppJsonSchema string
gioc81a8472024-09-24 13:06:19 +020046
gio9d66f322024-07-06 13:45:10 +040047const (
gioa60f0de2024-07-08 10:49:48 +040048 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040049 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040050 loginPath = "/login"
51 logoutPath = "/logout"
gio59946282024-10-07 12:55:51 +040052 staticPath = "/static/"
gioc81a8472024-09-24 13:06:19 +020053 schemasPath = "/schemas/"
gio8fae3af2024-07-25 13:43:31 +040054 apiPublicData = "/api/public-data"
55 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040056 sessionCookie = "dodo-app-session"
57 userCtx = "user"
giob4a3a192024-08-19 09:55:47 +040058 initCommitMsg = "init"
gio9d66f322024-07-06 13:45:10 +040059)
60
gio59946282024-10-07 12:55:51 +040061type tmplts struct {
giob4a3a192024-08-19 09:55:47 +040062 index *template.Template
63 appStatus *template.Template
64 commitStatus *template.Template
gio183e8342024-08-20 06:01:24 +040065 logs *template.Template
gio23bdc1b2024-07-11 16:07:47 +040066}
67
gio59946282024-10-07 12:55:51 +040068func parseTemplates(fs embed.FS) (tmplts, error) {
69 base, err := template.ParseFS(fs, "templates/base.html")
gio23bdc1b2024-07-11 16:07:47 +040070 if err != nil {
gio59946282024-10-07 12:55:51 +040071 return tmplts{}, err
gio23bdc1b2024-07-11 16:07:47 +040072 }
gio5e49bb62024-07-20 10:43:19 +040073 parse := func(path string) (*template.Template, error) {
74 if b, err := base.Clone(); err != nil {
75 return nil, err
76 } else {
77 return b.ParseFS(fs, path)
78 }
79 }
gio59946282024-10-07 12:55:51 +040080 index, err := parse("templates/index.html")
gio5e49bb62024-07-20 10:43:19 +040081 if err != nil {
gio59946282024-10-07 12:55:51 +040082 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040083 }
gio59946282024-10-07 12:55:51 +040084 appStatus, err := parse("templates/app_status.html")
gio5e49bb62024-07-20 10:43:19 +040085 if err != nil {
gio59946282024-10-07 12:55:51 +040086 return tmplts{}, err
gio5e49bb62024-07-20 10:43:19 +040087 }
gio59946282024-10-07 12:55:51 +040088 commitStatus, err := parse("templates/commit_status.html")
giob4a3a192024-08-19 09:55:47 +040089 if err != nil {
gio59946282024-10-07 12:55:51 +040090 return tmplts{}, err
giob4a3a192024-08-19 09:55:47 +040091 }
gio59946282024-10-07 12:55:51 +040092 logs, err := parse("templates/logs.html")
gio183e8342024-08-20 06:01:24 +040093 if err != nil {
gio59946282024-10-07 12:55:51 +040094 return tmplts{}, err
gio183e8342024-08-20 06:01:24 +040095 }
gio59946282024-10-07 12:55:51 +040096 return tmplts{index, appStatus, commitStatus, logs}, nil
gio23bdc1b2024-07-11 16:07:47 +040097}
98
gio59946282024-10-07 12:55:51 +040099type Server struct {
giocb34ad22024-07-11 08:01:13 +0400100 l sync.Locker
101 st Store
gio11617ac2024-07-15 16:09:04 +0400102 nf NetworkFilter
103 ug UserGetter
giocb34ad22024-07-11 08:01:13 +0400104 port int
105 apiPort int
106 self string
gioc81a8472024-09-24 13:06:19 +0200107 selfPublic string
gio11617ac2024-07-15 16:09:04 +0400108 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +0400109 sshKey string
110 gitRepoPublicKey string
111 client soft.Client
112 namespace string
113 envAppManagerAddr string
114 env installer.EnvConfig
115 nsc installer.NamespaceCreator
116 jc installer.JobCreator
gio864b4332024-09-05 13:56:47 +0400117 vpnKeyGen installer.VPNAPIClient
giof6ad2982024-08-23 17:42:49 +0400118 cnc installer.ClusterNetworkConfigurator
giocb34ad22024-07-11 08:01:13 +0400119 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +0400120 appConfigs map[string]appConfig
gio59946282024-10-07 12:55:51 +0400121 tmplts tmplts
gio5e49bb62024-07-20 10:43:19 +0400122 appTmpls AppTmplStore
giocafd4e62024-07-31 10:53:40 +0400123 external bool
124 fetchUsersAddr string
gio43b0f422024-08-21 10:40:13 +0400125 reconciler tasks.Reconciler
gio183e8342024-08-20 06:01:24 +0400126 logs map[string]string
gio9f6b27d2024-10-14 10:08:40 +0400127 schemaTmpl *ttemplate.Template
giod8ab4f52024-07-26 16:58:34 +0400128}
129
130type appConfig struct {
131 Namespace string `json:"namespace"`
132 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400133}
134
gio33059762024-07-05 13:19:07 +0400135// TODO(gio): Initialize appNs on startup
gio59946282024-10-07 12:55:51 +0400136func NewServer(
gioa60f0de2024-07-08 10:49:48 +0400137 st Store,
gio11617ac2024-07-15 16:09:04 +0400138 nf NetworkFilter,
139 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400140 port int,
gioa60f0de2024-07-08 10:49:48 +0400141 apiPort int,
gio33059762024-07-05 13:19:07 +0400142 self string,
gioc81a8472024-09-24 13:06:19 +0200143 selfPublic string,
gio11617ac2024-07-15 16:09:04 +0400144 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400145 sshKey string,
gio33059762024-07-05 13:19:07 +0400146 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400147 client soft.Client,
148 namespace string,
giocb34ad22024-07-11 08:01:13 +0400149 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400150 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400151 jc installer.JobCreator,
gio864b4332024-09-05 13:56:47 +0400152 vpnKeyGen installer.VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +0400153 cnc installer.ClusterNetworkConfigurator,
gio0eaf2712024-04-14 13:08:46 +0400154 env installer.EnvConfig,
giocafd4e62024-07-31 10:53:40 +0400155 external bool,
156 fetchUsersAddr string,
gio43b0f422024-08-21 10:40:13 +0400157 reconciler tasks.Reconciler,
gio59946282024-10-07 12:55:51 +0400158) (*Server, error) {
159 tmplts, err := parseTemplates(templates)
gio23bdc1b2024-07-11 16:07:47 +0400160 if err != nil {
161 return nil, err
162 }
gio5e4d1a72024-10-09 15:25:29 +0400163 apps, err := fs.Sub(appTmplsFS, "app-templates")
gio5e49bb62024-07-20 10:43:19 +0400164 if err != nil {
165 return nil, err
166 }
167 appTmpls, err := NewAppTmplStoreFS(apps)
168 if err != nil {
169 return nil, err
170 }
gio9f6b27d2024-10-14 10:08:40 +0400171 schemaTmpl, err := ttemplate.New("app.schema.json").Parse(dodoAppJsonSchema)
172 if err != nil {
173 return nil, err
174 }
gio59946282024-10-07 12:55:51 +0400175 s := &Server{
gio9d66f322024-07-06 13:45:10 +0400176 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400177 st,
gio11617ac2024-07-15 16:09:04 +0400178 nf,
179 ug,
gio0eaf2712024-04-14 13:08:46 +0400180 port,
gioa60f0de2024-07-08 10:49:48 +0400181 apiPort,
gio33059762024-07-05 13:19:07 +0400182 self,
gioc81a8472024-09-24 13:06:19 +0200183 selfPublic,
gio11617ac2024-07-15 16:09:04 +0400184 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400185 sshKey,
gio33059762024-07-05 13:19:07 +0400186 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400187 client,
188 namespace,
giocb34ad22024-07-11 08:01:13 +0400189 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400190 env,
gio33059762024-07-05 13:19:07 +0400191 nsc,
giof8843412024-05-22 16:38:05 +0400192 jc,
gio36b23b32024-08-25 12:20:54 +0400193 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +0400194 cnc,
gio266c04f2024-07-03 14:18:45 +0400195 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400196 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400197 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400198 appTmpls,
giocafd4e62024-07-31 10:53:40 +0400199 external,
200 fetchUsersAddr,
gio43b0f422024-08-21 10:40:13 +0400201 reconciler,
gio183e8342024-08-20 06:01:24 +0400202 map[string]string{},
gio9f6b27d2024-10-14 10:08:40 +0400203 schemaTmpl,
gio0eaf2712024-04-14 13:08:46 +0400204 }
gioa60f0de2024-07-08 10:49:48 +0400205 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400206 if err != nil {
207 return nil, err
208 }
giod8ab4f52024-07-26 16:58:34 +0400209 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400210 if err == nil {
211 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400212 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400213 return nil, err
214 }
215 } else if !errors.Is(err, fs.ErrNotExist) {
216 return nil, err
217 }
218 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400219}
220
gio59946282024-10-07 12:55:51 +0400221func (s *Server) getAppConfig(app, branch string) appConfig {
gio7fbd4ad2024-08-27 10:06:39 +0400222 return s.appConfigs[fmt.Sprintf("%s-%s", app, branch)]
223}
224
gio59946282024-10-07 12:55:51 +0400225func (s *Server) setAppConfig(app, branch string, cfg appConfig) {
gio7fbd4ad2024-08-27 10:06:39 +0400226 s.appConfigs[fmt.Sprintf("%s-%s", app, branch)] = cfg
227}
228
gio59946282024-10-07 12:55:51 +0400229func (s *Server) Start() error {
gio7fbd4ad2024-08-27 10:06:39 +0400230 // if err := s.client.DisableKeyless(); err != nil {
231 // return err
232 // }
233 // if err := s.client.DisableAnonAccess(); err != nil {
234 // return err
235 // }
gioa60f0de2024-07-08 10:49:48 +0400236 e := make(chan error)
237 go func() {
238 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400239 r.Use(s.mwAuth)
gio9f6b27d2024-10-14 10:08:40 +0400240 r.HandleFunc(schemasPath+"{user}/app.schema.json", s.handleSchema).Methods(http.MethodGet)
gio59946282024-10-07 12:55:51 +0400241 r.PathPrefix(staticPath).Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
gio81246f02024-07-10 12:02:15 +0400242 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400243 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
244 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400245 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
246 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
gio183e8342024-08-20 06:01:24 +0400247 r.HandleFunc("/{app-name}/logs", s.handleAppLogs).Methods(http.MethodGet)
giob4a3a192024-08-19 09:55:47 +0400248 r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
gio7fbd4ad2024-08-27 10:06:39 +0400249 r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
250 r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400251 r.HandleFunc("/{app-name}/branch/{branch}/delete", s.handleBranchDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400252 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
gio5887caa2024-10-03 15:07:23 +0400253 r.HandleFunc("/{app-name}/delete", s.handleAppDelete).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400254 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400255 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400256 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
257 }()
258 go func() {
259 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400260 r.HandleFunc("/update", s.handleAPIUpdate)
261 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
gio7fbd4ad2024-08-27 10:06:39 +0400262 r.HandleFunc("/api/add-public-key", s.handleAPIAddPublicKey).Methods(http.MethodPost)
giocfb228c2024-09-06 15:44:31 +0400263 r.HandleFunc("/api/apps/{app-name}/branch/{branch}/env-profile", s.handleBranchEnvProfile).Methods(http.MethodGet)
giocafd4e62024-07-31 10:53:40 +0400264 if !s.external {
265 r.HandleFunc("/api/sync-users", s.handleAPISyncUsers).Methods(http.MethodGet)
266 }
gioa60f0de2024-07-08 10:49:48 +0400267 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
268 }()
giocafd4e62024-07-31 10:53:40 +0400269 if !s.external {
270 go func() {
271 s.syncUsers()
Davit Tabidzea5ea5092024-08-01 15:28:09 +0400272 for {
273 delay := time.Duration(rand.Intn(60)+60) * time.Second
274 time.Sleep(delay)
giocafd4e62024-07-31 10:53:40 +0400275 s.syncUsers()
276 }
277 }()
278 }
gioa60f0de2024-07-08 10:49:48 +0400279 return <-e
280}
281
gio11617ac2024-07-15 16:09:04 +0400282type UserGetter interface {
283 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400284 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400285}
286
287type externalUserGetter struct {
288 sc *securecookie.SecureCookie
289}
290
291func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400292 return &externalUserGetter{securecookie.New(
293 securecookie.GenerateRandomKey(64),
294 securecookie.GenerateRandomKey(32),
295 )}
gio11617ac2024-07-15 16:09:04 +0400296}
297
298func (ug *externalUserGetter) Get(r *http.Request) string {
299 cookie, err := r.Cookie(sessionCookie)
300 if err != nil {
301 return ""
302 }
303 var user string
304 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
305 return ""
306 }
307 return user
308}
309
gio8fae3af2024-07-25 13:43:31 +0400310func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
311 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
312 cookie := &http.Cookie{
313 Name: sessionCookie,
314 Value: encoded,
315 Path: "/",
316 Secure: true,
317 HttpOnly: true,
318 }
319 http.SetCookie(w, cookie)
320 return nil
321 } else {
322 return err
323 }
324}
325
gio11617ac2024-07-15 16:09:04 +0400326type internalUserGetter struct{}
327
328func NewInternalUserGetter() UserGetter {
329 return internalUserGetter{}
330}
331
332func (ug internalUserGetter) Get(r *http.Request) string {
giodd213152024-09-27 11:26:59 +0200333 return r.Header.Get("X-Forwarded-User")
gio11617ac2024-07-15 16:09:04 +0400334}
335
gio8fae3af2024-07-25 13:43:31 +0400336func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
337 return nil
338}
339
gio59946282024-10-07 12:55:51 +0400340func (s *Server) mwAuth(next http.Handler) http.Handler {
gio81246f02024-07-10 12:02:15 +0400341 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400342 if strings.HasSuffix(r.URL.Path, loginPath) ||
343 strings.HasPrefix(r.URL.Path, logoutPath) ||
344 strings.HasPrefix(r.URL.Path, staticPath) ||
gioc81a8472024-09-24 13:06:19 +0200345 strings.HasPrefix(r.URL.Path, schemasPath) ||
gio8fae3af2024-07-25 13:43:31 +0400346 strings.HasPrefix(r.URL.Path, apiPublicData) ||
347 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400348 next.ServeHTTP(w, r)
349 return
350 }
gio11617ac2024-07-15 16:09:04 +0400351 user := s.ug.Get(r)
352 if user == "" {
gio81246f02024-07-10 12:02:15 +0400353 vars := mux.Vars(r)
354 appName, ok := vars["app-name"]
355 if !ok || appName == "" {
356 http.Error(w, "missing app-name", http.StatusBadRequest)
357 return
358 }
359 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
360 return
361 }
gio81246f02024-07-10 12:02:15 +0400362 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
363 })
364}
365
gio9f6b27d2024-10-14 10:08:40 +0400366type schemaNetwork struct {
367 Value string `json:"const"`
368}
369
gio59946282024-10-07 12:55:51 +0400370func (s *Server) handleSchema(w http.ResponseWriter, r *http.Request) {
gioc81a8472024-09-24 13:06:19 +0200371 w.Header().Set("Content-Type", "application/schema+json")
gio9f6b27d2024-10-14 10:08:40 +0400372 vars := mux.Vars(r)
373 user, ok := vars["user"]
374 if !ok {
375 http.Error(w, "no user", http.StatusBadRequest)
376 return
377 }
378 networks, err := s.getNetworks(user)
379 if err != nil {
380 http.Error(w, err.Error(), http.StatusInternalServerError)
381 return
382 }
383 var names []schemaNetwork
384 for _, n := range networks {
385 names = append(names, schemaNetwork{n.Name})
386 }
387 var tmp strings.Builder
388 if err := json.NewEncoder(&tmp).Encode(names); err != nil {
389 http.Error(w, err.Error(), http.StatusInternalServerError)
390 return
391 }
392 if err := s.schemaTmpl.Execute(w, map[string]any{
393 "Networks": tmp.String(),
394 }); err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
397 }
gioc81a8472024-09-24 13:06:19 +0200398}
399
gio59946282024-10-07 12:55:51 +0400400func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400401 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400402 http.SetCookie(w, &http.Cookie{
403 Name: sessionCookie,
404 Value: "",
405 Path: "/",
406 HttpOnly: true,
407 Secure: true,
408 })
409 http.Redirect(w, r, "/", http.StatusSeeOther)
410}
411
gio59946282024-10-07 12:55:51 +0400412func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400413 vars := mux.Vars(r)
414 appName, ok := vars["app-name"]
415 if !ok || appName == "" {
416 http.Error(w, "missing app-name", http.StatusBadRequest)
417 return
418 }
419 fmt.Fprint(w, `
420<!DOCTYPE html>
421<html lang='en'>
422 <head>
423 <title>dodo: app - login</title>
424 <meta charset='utf-8'>
425 </head>
426 <body>
427 <form action="" method="POST">
428 <input type="password" placeholder="Password" name="password" required />
429 <button type="submit">Login</button>
430 </form>
431 </body>
432</html>
433`)
434}
435
gio59946282024-10-07 12:55:51 +0400436func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400437 vars := mux.Vars(r)
438 appName, ok := vars["app-name"]
439 if !ok || appName == "" {
440 http.Error(w, "missing app-name", http.StatusBadRequest)
441 return
442 }
443 password := r.FormValue("password")
444 if password == "" {
445 http.Error(w, "missing password", http.StatusBadRequest)
446 return
447 }
448 user, err := s.st.GetAppOwner(appName)
449 if err != nil {
450 http.Error(w, err.Error(), http.StatusInternalServerError)
451 return
452 }
453 hashed, err := s.st.GetUserPassword(user)
454 if err != nil {
455 http.Error(w, err.Error(), http.StatusInternalServerError)
456 return
457 }
458 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
459 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
460 return
461 }
gio8fae3af2024-07-25 13:43:31 +0400462 if err := s.ug.Encode(w, user); err != nil {
463 http.Error(w, err.Error(), http.StatusInternalServerError)
464 return
gio81246f02024-07-10 12:02:15 +0400465 }
466 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
467}
468
giob4a3a192024-08-19 09:55:47 +0400469type navItem struct {
470 Name string
471 Address string
472}
473
gio23bdc1b2024-07-11 16:07:47 +0400474type statusData struct {
giob4a3a192024-08-19 09:55:47 +0400475 Navigation []navItem
476 Apps []string
477 Networks []installer.Network
478 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400479}
480
gio59946282024-10-07 12:55:51 +0400481func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400482 user := r.Context().Value(userCtx)
483 if user == nil {
484 http.Error(w, "unauthorized", http.StatusUnauthorized)
485 return
486 }
gio281aa512024-10-25 23:25:53 +0400487 var apps []string
488 var err error
489 if s.external {
490 apps, err = s.st.GetUserApps(user.(string))
491 } else {
492 apps, err = s.st.GetApps()
493 }
gioa60f0de2024-07-08 10:49:48 +0400494 if err != nil {
495 http.Error(w, err.Error(), http.StatusInternalServerError)
496 return
497 }
gio11617ac2024-07-15 16:09:04 +0400498 networks, err := s.getNetworks(user.(string))
499 if err != nil {
500 http.Error(w, err.Error(), http.StatusInternalServerError)
501 return
502 }
giob54db242024-07-30 18:49:33 +0400503 var types []string
504 for _, t := range s.appTmpls.Types() {
505 types = append(types, strings.Replace(t, "-", ":", 1))
506 }
giob4a3a192024-08-19 09:55:47 +0400507 n := []navItem{navItem{"Home", "/"}}
508 data := statusData{n, apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400509 if err := s.tmplts.index.Execute(w, data); err != nil {
510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return
gioa60f0de2024-07-08 10:49:48 +0400512 }
513}
514
gio5e49bb62024-07-20 10:43:19 +0400515type appStatusData struct {
giob4a3a192024-08-19 09:55:47 +0400516 Navigation []navItem
gio5e49bb62024-07-20 10:43:19 +0400517 Name string
gio5887caa2024-10-03 15:07:23 +0400518 Branch string
gio5e49bb62024-07-20 10:43:19 +0400519 GitCloneCommand string
giob4a3a192024-08-19 09:55:47 +0400520 Commits []CommitMeta
gio183e8342024-08-20 06:01:24 +0400521 LastCommit resourceData
gio7fbd4ad2024-08-27 10:06:39 +0400522 Branches []string
gio5e49bb62024-07-20 10:43:19 +0400523}
524
gio59946282024-10-07 12:55:51 +0400525func (s *Server) handleAppStatus(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400526 vars := mux.Vars(r)
527 appName, ok := vars["app-name"]
528 if !ok || appName == "" {
529 http.Error(w, "missing app-name", http.StatusBadRequest)
530 return
531 }
gio7fbd4ad2024-08-27 10:06:39 +0400532 branch, ok := vars["branch"]
533 if !ok || branch == "" {
534 branch = "master"
535 }
gio94904702024-07-26 16:58:34 +0400536 u := r.Context().Value(userCtx)
537 if u == nil {
538 http.Error(w, "unauthorized", http.StatusUnauthorized)
539 return
540 }
541 user, ok := u.(string)
542 if !ok {
543 http.Error(w, "could not get user", http.StatusInternalServerError)
544 return
545 }
546 owner, err := s.st.GetAppOwner(appName)
547 if err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
550 }
gio281aa512024-10-25 23:25:53 +0400551 if s.external && owner != user {
gio94904702024-07-26 16:58:34 +0400552 http.Error(w, "unauthorized", http.StatusUnauthorized)
553 return
554 }
gio7fbd4ad2024-08-27 10:06:39 +0400555 commits, err := s.st.GetCommitHistory(appName, branch)
gioa60f0de2024-07-08 10:49:48 +0400556 if err != nil {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
gio183e8342024-08-20 06:01:24 +0400560 var lastCommitResources resourceData
561 if len(commits) > 0 {
562 lastCommit, err := s.st.GetCommit(commits[len(commits)-1].Hash)
563 if err != nil {
564 http.Error(w, err.Error(), http.StatusInternalServerError)
565 return
566 }
gio85958d62024-10-26 09:14:01 +0400567 r, err := extractResourceData(lastCommit.Resources)
gio183e8342024-08-20 06:01:24 +0400568 if err != nil {
569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
571 }
572 lastCommitResources = r
573 }
gio7fbd4ad2024-08-27 10:06:39 +0400574 branches, err := s.st.GetBranches(appName)
575 if err != nil {
576 http.Error(w, err.Error(), http.StatusInternalServerError)
577 return
578 }
gio5e49bb62024-07-20 10:43:19 +0400579 data := appStatusData{
giob4a3a192024-08-19 09:55:47 +0400580 Navigation: []navItem{
581 navItem{"Home", "/"},
582 navItem{appName, "/" + appName},
583 },
gio5e49bb62024-07-20 10:43:19 +0400584 Name: appName,
gio5887caa2024-10-03 15:07:23 +0400585 Branch: branch,
gio5e49bb62024-07-20 10:43:19 +0400586 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
587 Commits: commits,
gio183e8342024-08-20 06:01:24 +0400588 LastCommit: lastCommitResources,
gio7fbd4ad2024-08-27 10:06:39 +0400589 Branches: branches,
590 }
591 if branch != "master" {
592 data.Navigation = append(data.Navigation, navItem{branch, fmt.Sprintf("/%s/branch/%s", appName, branch)})
gio5e49bb62024-07-20 10:43:19 +0400593 }
594 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
595 http.Error(w, err.Error(), http.StatusInternalServerError)
596 return
gioa60f0de2024-07-08 10:49:48 +0400597 }
gio0eaf2712024-04-14 13:08:46 +0400598}
599
giocfb228c2024-09-06 15:44:31 +0400600type appEnv struct {
601 Profile string `json:"envProfile"`
602}
603
gio59946282024-10-07 12:55:51 +0400604func (s *Server) handleBranchEnvProfile(w http.ResponseWriter, r *http.Request) {
giocfb228c2024-09-06 15:44:31 +0400605 vars := mux.Vars(r)
606 appName, ok := vars["app-name"]
607 if !ok || appName == "" {
608 http.Error(w, "missing app-name", http.StatusBadRequest)
609 return
610 }
611 branch, ok := vars["branch"]
612 if !ok || branch == "" {
613 branch = "master"
614 }
615 info, err := s.st.GetLastCommitInfo(appName, branch)
616 if err != nil {
617 http.Error(w, err.Error(), http.StatusInternalServerError)
618 return
619 }
620 var e appEnv
621 if err := json.NewDecoder(bytes.NewReader(info.Resources.RenderedRaw)).Decode(&e); err != nil {
622 http.Error(w, err.Error(), http.StatusInternalServerError)
623 return
624 }
625 fmt.Fprintln(w, e.Profile)
626}
627
giob4a3a192024-08-19 09:55:47 +0400628type volume struct {
629 Name string
630 Size string
631}
632
633type postgresql struct {
634 Name string
635 Version string
636 Volume string
637}
638
gio07eb1082024-10-25 14:35:56 +0400639type mongodb struct {
640 Name string
641 Version string
642 Volume string
643}
644
giob4a3a192024-08-19 09:55:47 +0400645type ingress struct {
giof078f462024-10-14 09:07:33 +0400646 Name string
giob4a3a192024-08-19 09:55:47 +0400647 Host string
giof078f462024-10-14 09:07:33 +0400648 Home string
giob4a3a192024-08-19 09:55:47 +0400649}
650
gio7fbd4ad2024-08-27 10:06:39 +0400651type vm struct {
652 Name string
653 User string
654 CPUCores int
655 Memory string
656}
657
giob4a3a192024-08-19 09:55:47 +0400658type resourceData struct {
gio7fbd4ad2024-08-27 10:06:39 +0400659 Volume []volume
660 PostgreSQL []postgresql
gio07eb1082024-10-25 14:35:56 +0400661 MongoDB []mongodb
gio7fbd4ad2024-08-27 10:06:39 +0400662 Ingress []ingress
663 VirtualMachine []vm
gio85958d62024-10-26 09:14:01 +0400664 Env []string
giob4a3a192024-08-19 09:55:47 +0400665}
666
667type commitStatusData struct {
668 Navigation []navItem
669 AppName string
670 Commit Commit
671 Resources resourceData
672}
673
gio59946282024-10-07 12:55:51 +0400674func (s *Server) handleAppCommit(w http.ResponseWriter, r *http.Request) {
giob4a3a192024-08-19 09:55:47 +0400675 vars := mux.Vars(r)
676 appName, ok := vars["app-name"]
677 if !ok || appName == "" {
678 http.Error(w, "missing app-name", http.StatusBadRequest)
679 return
680 }
681 hash, ok := vars["hash"]
682 if !ok || appName == "" {
683 http.Error(w, "missing app-name", http.StatusBadRequest)
684 return
685 }
686 u := r.Context().Value(userCtx)
687 if u == nil {
688 http.Error(w, "unauthorized", http.StatusUnauthorized)
689 return
690 }
691 user, ok := u.(string)
692 if !ok {
693 http.Error(w, "could not get user", http.StatusInternalServerError)
694 return
695 }
696 owner, err := s.st.GetAppOwner(appName)
697 if err != nil {
698 http.Error(w, err.Error(), http.StatusInternalServerError)
699 return
700 }
gio281aa512024-10-25 23:25:53 +0400701 if s.external && owner != user {
giob4a3a192024-08-19 09:55:47 +0400702 http.Error(w, "unauthorized", http.StatusUnauthorized)
703 return
704 }
705 commit, err := s.st.GetCommit(hash)
706 if err != nil {
707 // TODO(gio): not-found ?
708 http.Error(w, err.Error(), http.StatusInternalServerError)
709 return
710 }
711 var res strings.Builder
712 if err := json.NewEncoder(&res).Encode(commit.Resources.Helm); err != nil {
713 http.Error(w, err.Error(), http.StatusInternalServerError)
714 return
715 }
gio85958d62024-10-26 09:14:01 +0400716 resData, err := extractResourceData(commit.Resources)
giob4a3a192024-08-19 09:55:47 +0400717 if err != nil {
718 http.Error(w, err.Error(), http.StatusInternalServerError)
719 return
720 }
721 data := commitStatusData{
722 Navigation: []navItem{
723 navItem{"Home", "/"},
724 navItem{appName, "/" + appName},
725 navItem{hash, "/" + appName + "/" + hash},
726 },
727 AppName: appName,
728 Commit: commit,
729 Resources: resData,
730 }
731 if err := s.tmplts.commitStatus.Execute(w, data); err != nil {
732 http.Error(w, err.Error(), http.StatusInternalServerError)
733 return
734 }
735}
736
gio183e8342024-08-20 06:01:24 +0400737type logData struct {
738 Navigation []navItem
739 AppName string
740 Logs template.HTML
741}
742
gio59946282024-10-07 12:55:51 +0400743func (s *Server) handleAppLogs(w http.ResponseWriter, r *http.Request) {
gio183e8342024-08-20 06:01:24 +0400744 vars := mux.Vars(r)
745 appName, ok := vars["app-name"]
746 if !ok || appName == "" {
747 http.Error(w, "missing app-name", http.StatusBadRequest)
748 return
749 }
750 u := r.Context().Value(userCtx)
751 if u == nil {
752 http.Error(w, "unauthorized", http.StatusUnauthorized)
753 return
754 }
755 user, ok := u.(string)
756 if !ok {
757 http.Error(w, "could not get user", http.StatusInternalServerError)
758 return
759 }
760 owner, err := s.st.GetAppOwner(appName)
761 if err != nil {
762 http.Error(w, err.Error(), http.StatusInternalServerError)
763 return
764 }
gio281aa512024-10-25 23:25:53 +0400765 if s.external && owner != user {
gio183e8342024-08-20 06:01:24 +0400766 http.Error(w, "unauthorized", http.StatusUnauthorized)
767 return
768 }
769 data := logData{
770 Navigation: []navItem{
771 navItem{"Home", "/"},
772 navItem{appName, "/" + appName},
773 navItem{"Logs", "/" + appName + "/logs"},
774 },
775 AppName: appName,
776 Logs: template.HTML(strings.ReplaceAll(s.logs[appName], "\n", "<br/>")),
777 }
778 if err := s.tmplts.logs.Execute(w, data); err != nil {
779 fmt.Println(err)
780 http.Error(w, err.Error(), http.StatusInternalServerError)
781 return
782 }
783}
784
gio81246f02024-07-10 12:02:15 +0400785type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400786 Ref string `json:"ref"`
787 Repository struct {
788 Name string `json:"name"`
789 } `json:"repository"`
gioe2e31e12024-08-18 08:20:56 +0400790 After string `json:"after"`
791 Commits []struct {
792 Id string `json:"id"`
793 Message string `json:"message"`
794 } `json:"commits"`
gio0eaf2712024-04-14 13:08:46 +0400795}
796
gio59946282024-10-07 12:55:51 +0400797func (s *Server) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400798 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400799 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400800 var contents strings.Builder
801 io.Copy(&contents, r.Body)
802 c := contents.String()
803 fmt.Println(c)
804 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400805 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400806 return
807 }
gio7fbd4ad2024-08-27 10:06:39 +0400808 if strings.HasPrefix(req.Ref, "refs/heads/dodo_") || req.Repository.Name == ConfigRepoName {
809 return
810 }
811 branch, ok := strings.CutPrefix(req.Ref, "refs/heads/")
812 if !ok {
813 http.Error(w, "invalid branch", http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400814 return
815 }
gioa60f0de2024-07-08 10:49:48 +0400816 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400817 go func() {
gio11617ac2024-07-15 16:09:04 +0400818 owner, err := s.st.GetAppOwner(req.Repository.Name)
819 if err != nil {
820 return
821 }
822 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400823 if err != nil {
824 return
825 }
giof15b9da2024-09-19 06:59:16 +0400826 // TODO(gio): get only available ones by owner
827 clusters, err := s.getClusters()
828 if err != nil {
829 return
830 }
gio94904702024-07-26 16:58:34 +0400831 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
832 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
833 if err != nil {
834 return
835 }
gioe2e31e12024-08-18 08:20:56 +0400836 found := false
837 commitMsg := ""
838 for _, c := range req.Commits {
839 if c.Id == req.After {
840 found = true
841 commitMsg = c.Message
842 break
gioa60f0de2024-07-08 10:49:48 +0400843 }
844 }
gioe2e31e12024-08-18 08:20:56 +0400845 if !found {
846 fmt.Printf("Error: could not find commit message")
847 return
848 }
gio7fbd4ad2024-08-27 10:06:39 +0400849 s.l.Lock()
850 defer s.l.Unlock()
giof15b9da2024-09-19 06:59:16 +0400851 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 +0400852 if err = s.createCommit(req.Repository.Name, branch, req.After, commitMsg, err, resources); err != nil {
gio12e887d2024-08-18 16:09:47 +0400853 fmt.Printf("Error: %s\n", err.Error())
gioe2e31e12024-08-18 08:20:56 +0400854 return
855 }
gioa60f0de2024-07-08 10:49:48 +0400856 for addr, _ := range s.workers[req.Repository.Name] {
857 go func() {
858 // TODO(gio): make port configurable
859 http.Get(fmt.Sprintf("http://%s/update", addr))
860 }()
gio0eaf2712024-04-14 13:08:46 +0400861 }
862 }()
gio0eaf2712024-04-14 13:08:46 +0400863}
864
gio81246f02024-07-10 12:02:15 +0400865type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400866 Address string `json:"address"`
gio183e8342024-08-20 06:01:24 +0400867 Logs string `json:"logs"`
gio0eaf2712024-04-14 13:08:46 +0400868}
869
gio59946282024-10-07 12:55:51 +0400870func (s *Server) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400871 // TODO(gio): lock
gioa60f0de2024-07-08 10:49:48 +0400872 vars := mux.Vars(r)
873 appName, ok := vars["app-name"]
874 if !ok || appName == "" {
875 http.Error(w, "missing app-name", http.StatusBadRequest)
876 return
877 }
gio81246f02024-07-10 12:02:15 +0400878 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400879 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
880 http.Error(w, err.Error(), http.StatusInternalServerError)
881 return
882 }
gioa60f0de2024-07-08 10:49:48 +0400883 if _, ok := s.workers[appName]; !ok {
884 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400885 }
gioa60f0de2024-07-08 10:49:48 +0400886 s.workers[appName][req.Address] = struct{}{}
gio183e8342024-08-20 06:01:24 +0400887 s.logs[appName] = req.Logs
gio0eaf2712024-04-14 13:08:46 +0400888}
889
gio59946282024-10-07 12:55:51 +0400890func (s *Server) handleCreateApp(w http.ResponseWriter, r *http.Request) {
gio11617ac2024-07-15 16:09:04 +0400891 u := r.Context().Value(userCtx)
892 if u == nil {
893 http.Error(w, "unauthorized", http.StatusUnauthorized)
894 return
895 }
896 user, ok := u.(string)
897 if !ok {
898 http.Error(w, "could not get user", http.StatusInternalServerError)
899 return
900 }
901 network := r.FormValue("network")
902 if network == "" {
903 http.Error(w, "missing network", http.StatusBadRequest)
904 return
905 }
gio5e49bb62024-07-20 10:43:19 +0400906 subdomain := r.FormValue("subdomain")
907 if subdomain == "" {
908 http.Error(w, "missing subdomain", http.StatusBadRequest)
909 return
910 }
911 appType := r.FormValue("type")
912 if appType == "" {
913 http.Error(w, "missing type", http.StatusBadRequest)
914 return
915 }
gio5cc6afc2024-10-06 09:33:44 +0400916 appName := r.FormValue("name")
917 var err error
918 if appName == "" {
919 g := installer.NewFixedLengthRandomNameGenerator(3)
920 appName, err = g.Generate()
921 }
gio11617ac2024-07-15 16:09:04 +0400922 if err != nil {
923 http.Error(w, err.Error(), http.StatusInternalServerError)
924 return
925 }
926 if ok, err := s.client.UserExists(user); err != nil {
927 http.Error(w, err.Error(), http.StatusInternalServerError)
928 return
929 } else if !ok {
giocafd4e62024-07-31 10:53:40 +0400930 http.Error(w, "user sync has not finished, please try again in few minutes", http.StatusFailedDependency)
931 return
gio11617ac2024-07-15 16:09:04 +0400932 }
giocafd4e62024-07-31 10:53:40 +0400933 if err := s.st.CreateUser(user, nil, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
gio11617ac2024-07-15 16:09:04 +0400934 http.Error(w, err.Error(), http.StatusInternalServerError)
935 return
936 }
937 if err := s.st.CreateApp(appName, user); err != nil {
938 http.Error(w, err.Error(), http.StatusInternalServerError)
939 return
940 }
giod8ab4f52024-07-26 16:58:34 +0400941 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400942 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) handleCreateDevBranch(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400949 u := r.Context().Value(userCtx)
950 if u == nil {
951 http.Error(w, "unauthorized", http.StatusUnauthorized)
952 return
953 }
954 user, ok := u.(string)
955 if !ok {
956 http.Error(w, "could not get user", http.StatusInternalServerError)
957 return
958 }
959 vars := mux.Vars(r)
960 appName, ok := vars["app-name"]
961 if !ok || appName == "" {
962 http.Error(w, "missing app-name", http.StatusBadRequest)
963 return
964 }
965 branch := r.FormValue("branch")
966 if branch == "" {
gio5887caa2024-10-03 15:07:23 +0400967 http.Error(w, "missing branch", http.StatusBadRequest)
gio7fbd4ad2024-08-27 10:06:39 +0400968 return
969 }
970 if err := s.createDevBranch(appName, "master", branch, user); err != nil {
971 http.Error(w, err.Error(), http.StatusInternalServerError)
972 return
973 }
974 http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
975}
976
gio59946282024-10-07 12:55:51 +0400977func (s *Server) handleBranchDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +0400978 u := r.Context().Value(userCtx)
979 if u == nil {
980 http.Error(w, "unauthorized", http.StatusUnauthorized)
981 return
982 }
983 vars := mux.Vars(r)
984 appName, ok := vars["app-name"]
985 if !ok || appName == "" {
986 http.Error(w, "missing app-name", http.StatusBadRequest)
987 return
988 }
989 branch, ok := vars["branch"]
990 if !ok || branch == "" {
991 http.Error(w, "missing branch", http.StatusBadRequest)
992 return
993 }
994 if err := s.deleteBranch(appName, branch); err != nil {
995 http.Error(w, err.Error(), http.StatusInternalServerError)
996 return
997 }
998 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
999}
1000
gio59946282024-10-07 12:55:51 +04001001func (s *Server) handleAppDelete(w http.ResponseWriter, r *http.Request) {
gio5887caa2024-10-03 15:07:23 +04001002 u := r.Context().Value(userCtx)
1003 if u == nil {
1004 http.Error(w, "unauthorized", http.StatusUnauthorized)
1005 return
1006 }
1007 vars := mux.Vars(r)
1008 appName, ok := vars["app-name"]
1009 if !ok || appName == "" {
1010 http.Error(w, "missing app-name", http.StatusBadRequest)
1011 return
1012 }
1013 if err := s.deleteApp(appName); err != nil {
1014 http.Error(w, err.Error(), http.StatusInternalServerError)
1015 return
1016 }
gioe44c1512024-10-06 14:13:55 +04001017 http.Redirect(w, r, "/", http.StatusSeeOther)
gio5887caa2024-10-03 15:07:23 +04001018}
1019
gio81246f02024-07-10 12:02:15 +04001020type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +04001021 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +04001022 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +04001023 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +04001024 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +04001025}
1026
gio81246f02024-07-10 12:02:15 +04001027type apiCreateAppResp struct {
1028 AppName string `json:"appName"`
1029 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +04001030}
1031
gio59946282024-10-07 12:55:51 +04001032func (s *Server) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001033 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +04001034 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +04001035 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1036 http.Error(w, err.Error(), http.StatusBadRequest)
1037 return
1038 }
1039 g := installer.NewFixedLengthRandomNameGenerator(3)
1040 appName, err := g.Generate()
1041 if err != nil {
1042 http.Error(w, err.Error(), http.StatusInternalServerError)
1043 return
1044 }
gio11617ac2024-07-15 16:09:04 +04001045 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +04001046 if err != nil {
gio33059762024-07-05 13:19:07 +04001047 http.Error(w, err.Error(), http.StatusInternalServerError)
1048 return
1049 }
gio11617ac2024-07-15 16:09:04 +04001050 if user != "" {
1051 http.Error(w, "public key already registered", http.StatusBadRequest)
1052 return
1053 }
1054 user = appName
1055 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
1056 http.Error(w, err.Error(), http.StatusInternalServerError)
1057 return
1058 }
1059 password := generatePassword()
1060 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
1061 if err != nil {
1062 http.Error(w, err.Error(), http.StatusInternalServerError)
1063 return
1064 }
giocafd4e62024-07-31 10:53:40 +04001065 if err := s.st.CreateUser(user, hashed, req.Network); err != nil {
gio11617ac2024-07-15 16:09:04 +04001066 http.Error(w, err.Error(), http.StatusInternalServerError)
1067 return
1068 }
1069 if err := s.st.CreateApp(appName, user); err != nil {
1070 http.Error(w, err.Error(), http.StatusInternalServerError)
1071 return
1072 }
giod8ab4f52024-07-26 16:58:34 +04001073 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +04001074 http.Error(w, err.Error(), http.StatusInternalServerError)
1075 return
1076 }
gio81246f02024-07-10 12:02:15 +04001077 resp := apiCreateAppResp{
1078 AppName: appName,
1079 Password: password,
1080 }
gio33059762024-07-05 13:19:07 +04001081 if err := json.NewEncoder(w).Encode(resp); err != nil {
1082 http.Error(w, err.Error(), http.StatusInternalServerError)
1083 return
1084 }
1085}
1086
gio59946282024-10-07 12:55:51 +04001087func (s *Server) isNetworkUseAllowed(network string) bool {
giocafd4e62024-07-31 10:53:40 +04001088 if !s.external {
giod8ab4f52024-07-26 16:58:34 +04001089 return true
1090 }
1091 for _, cfg := range s.appConfigs {
1092 if strings.ToLower(cfg.Network) == network {
1093 return false
1094 }
1095 }
1096 return true
1097}
1098
gio59946282024-10-07 12:55:51 +04001099func (s *Server) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +04001100 s.l.Lock()
1101 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +04001102 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +04001103 network = strings.ToLower(network)
1104 if !s.isNetworkUseAllowed(network) {
1105 return fmt.Errorf("network already used: %s", network)
1106 }
gio33059762024-07-05 13:19:07 +04001107 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001108 return err
gio33059762024-07-05 13:19:07 +04001109 } else if ok {
gio11617ac2024-07-15 16:09:04 +04001110 return nil
gioa60f0de2024-07-08 10:49:48 +04001111 }
gio5e49bb62024-07-20 10:43:19 +04001112 networks, err := s.getNetworks(user)
1113 if err != nil {
1114 return err
1115 }
giod8ab4f52024-07-26 16:58:34 +04001116 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +04001117 if !ok {
1118 return fmt.Errorf("network not found: %s\n", network)
1119 }
gio33059762024-07-05 13:19:07 +04001120 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +04001121 return err
gio33059762024-07-05 13:19:07 +04001122 }
1123 appRepo, err := s.client.GetRepo(appName)
1124 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001125 return err
gio33059762024-07-05 13:19:07 +04001126 }
gio9f6b27d2024-10-14 10:08:40 +04001127 files, err := s.renderAppConfigTemplate(user, appType, n, subdomain)
gio7fbd4ad2024-08-27 10:06:39 +04001128 if err != nil {
1129 return err
1130 }
1131 return s.createAppForBranch(appRepo, appName, "master", user, network, files)
1132}
1133
gio59946282024-10-07 12:55:51 +04001134func (s *Server) createDevBranch(appName, fromBranch, toBranch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001135 s.l.Lock()
1136 defer s.l.Unlock()
1137 fmt.Printf("Creating dev branch app: %s %s %s\n", appName, fromBranch, toBranch)
1138 appRepo, err := s.client.GetRepoBranch(appName, fromBranch)
1139 if err != nil {
1140 return err
1141 }
gioc81a8472024-09-24 13:06:19 +02001142 appCfg, err := soft.ReadFile(appRepo, "app.json")
gio7fbd4ad2024-08-27 10:06:39 +04001143 if err != nil {
1144 return err
1145 }
gio9f6b27d2024-10-14 10:08:40 +04001146 network, branchCfg, err := createDevBranchAppConfig(appCfg, toBranch, user, s.selfPublic)
gio7fbd4ad2024-08-27 10:06:39 +04001147 if err != nil {
1148 return err
1149 }
gioc81a8472024-09-24 13:06:19 +02001150 return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
gio7fbd4ad2024-08-27 10:06:39 +04001151}
1152
gio59946282024-10-07 12:55:51 +04001153func (s *Server) deleteBranch(appName string, branch string) error {
gio5887caa2024-10-03 15:07:23 +04001154 appBranch := fmt.Sprintf("dodo_%s", branch)
1155 hf := installer.NewGitHelmFetcher()
1156 if err := func() error {
1157 repo, err := s.client.GetRepoBranch(appName, appBranch)
1158 if err != nil {
1159 return err
1160 }
1161 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
1162 if err != nil {
1163 return err
1164 }
1165 return m.Remove("app")
1166 }(); err != nil {
1167 return err
1168 }
1169 configRepo, err := s.client.GetRepo(ConfigRepoName)
1170 if err != nil {
1171 return err
1172 }
1173 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
1174 if err != nil {
1175 return err
1176 }
1177 appPath := fmt.Sprintf("%s/%s", appName, branch)
gio829b1b72024-10-05 21:50:56 +04001178 if err := m.Remove(appPath); err != nil {
gio5887caa2024-10-03 15:07:23 +04001179 return err
1180 }
1181 if err := s.client.DeleteRepoBranch(appName, appBranch); err != nil {
1182 return err
1183 }
1184 if branch != "master" {
gioe44c1512024-10-06 14:13:55 +04001185 if err := s.client.DeleteRepoBranch(appName, branch); err != nil {
1186 return err
1187 }
gio5887caa2024-10-03 15:07:23 +04001188 }
gioe44c1512024-10-06 14:13:55 +04001189 return s.st.DeleteBranch(appName, branch)
gio5887caa2024-10-03 15:07:23 +04001190}
1191
gio59946282024-10-07 12:55:51 +04001192func (s *Server) deleteApp(appName string) error {
gio5887caa2024-10-03 15:07:23 +04001193 configRepo, err := s.client.GetRepo(ConfigRepoName)
1194 if err != nil {
1195 return err
1196 }
1197 branches, err := configRepo.ListDir(fmt.Sprintf("/%s", appName))
1198 if err != nil {
1199 return err
1200 }
1201 for _, b := range branches {
1202 if !b.IsDir() || strings.HasPrefix(b.Name(), "dodo_") {
1203 continue
1204 }
1205 if err := s.deleteBranch(appName, b.Name()); err != nil {
1206 return err
1207 }
1208 }
gioe44c1512024-10-06 14:13:55 +04001209 if err := s.client.DeleteRepo(appName); err != nil {
1210 return err
1211 }
1212 return s.st.DeleteApp(appName)
gio5887caa2024-10-03 15:07:23 +04001213}
1214
gio59946282024-10-07 12:55:51 +04001215func (s *Server) createAppForBranch(
gio7fbd4ad2024-08-27 10:06:39 +04001216 repo soft.RepoIO,
1217 appName string,
1218 branch string,
1219 user string,
1220 network string,
1221 files map[string][]byte,
1222) error {
1223 commit, err := repo.Do(func(fs soft.RepoFS) (string, error) {
1224 for path, contents := range files {
1225 if err := soft.WriteFile(fs, path, string(contents)); err != nil {
1226 return "", err
1227 }
1228 }
1229 return "init", nil
1230 }, soft.WithCommitToBranch(branch))
1231 if err != nil {
1232 return err
1233 }
1234 networks, err := s.getNetworks(user)
giob4a3a192024-08-19 09:55:47 +04001235 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001236 return err
gio33059762024-07-05 13:19:07 +04001237 }
giof15b9da2024-09-19 06:59:16 +04001238 // TODO(gio): get only available ones by owner
1239 clusters, err := s.getClusters()
1240 if err != nil {
1241 return err
1242 }
gio33059762024-07-05 13:19:07 +04001243 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +04001244 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
1245 if err != nil {
1246 return err
1247 }
1248 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +04001249 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001250 return err
gio33059762024-07-05 13:19:07 +04001251 }
1252 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
1253 suffix, err := suffixGen.Generate()
1254 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001255 return err
gio33059762024-07-05 13:19:07 +04001256 }
gio94904702024-07-26 16:58:34 +04001257 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
gio7fbd4ad2024-08-27 10:06:39 +04001258 s.setAppConfig(appName, branch, appConfig{namespace, network})
giof15b9da2024-09-19 06:59:16 +04001259 resources, err := s.updateDodoApp(instanceAppStatus, appName, branch, namespace, networks, clusters, user)
giob4a3a192024-08-19 09:55:47 +04001260 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001261 fmt.Printf("Error: %s\n", err.Error())
giob4a3a192024-08-19 09:55:47 +04001262 return err
1263 }
gio7fbd4ad2024-08-27 10:06:39 +04001264 if err = s.createCommit(appName, branch, commit, initCommitMsg, err, resources); err != nil {
giob4a3a192024-08-19 09:55:47 +04001265 fmt.Printf("Error: %s\n", err.Error())
gio11617ac2024-07-15 16:09:04 +04001266 return err
gio33059762024-07-05 13:19:07 +04001267 }
giod8ab4f52024-07-26 16:58:34 +04001268 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +04001269 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001270 return err
gio33059762024-07-05 13:19:07 +04001271 }
1272 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001273 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
gio33059762024-07-05 13:19:07 +04001274 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001275 return err
gio33059762024-07-05 13:19:07 +04001276 }
gio7fbd4ad2024-08-27 10:06:39 +04001277 appPath := fmt.Sprintf("/%s/%s", appName, branch)
giob4a3a192024-08-19 09:55:47 +04001278 _, err = configRepo.Do(func(fs soft.RepoFS) (string, error) {
giod8ab4f52024-07-26 16:58:34 +04001279 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +04001280 if err != nil {
1281 return "", err
1282 }
1283 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +04001284 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +04001285 return "", err
1286 }
1287 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +04001288 instanceApp,
gio9d66f322024-07-06 13:45:10 +04001289 appName,
gio7fbd4ad2024-08-27 10:06:39 +04001290 appPath,
gio9d66f322024-07-06 13:45:10 +04001291 namespace,
1292 map[string]any{
1293 "repoAddr": s.client.GetRepoAddress(appName),
1294 "repoHost": strings.Split(s.client.Address(), ":")[0],
gio7fbd4ad2024-08-27 10:06:39 +04001295 "branch": fmt.Sprintf("dodo_%s", branch),
gio9d66f322024-07-06 13:45:10 +04001296 "gitRepoPublicKey": s.gitRepoPublicKey,
1297 },
1298 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +04001299 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +04001300 installer.WithNoPublish(),
1301 installer.WithNoLock(),
1302 ); err != nil {
1303 return "", err
1304 }
1305 return fmt.Sprintf("Installed app: %s", appName), nil
giob4a3a192024-08-19 09:55:47 +04001306 })
1307 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001308 return err
gio33059762024-07-05 13:19:07 +04001309 }
gio7fbd4ad2024-08-27 10:06:39 +04001310 return s.initAppACLs(m, appPath, appName, branch, user)
1311}
1312
gio59946282024-10-07 12:55:51 +04001313func (s *Server) initAppACLs(m *installer.AppManager, path, appName, branch, user string) error {
gio7fbd4ad2024-08-27 10:06:39 +04001314 cfg, err := m.GetInstance(path)
gio33059762024-07-05 13:19:07 +04001315 if err != nil {
gio11617ac2024-07-15 16:09:04 +04001316 return err
gio33059762024-07-05 13:19:07 +04001317 }
1318 fluxKeys, ok := cfg.Input["fluxKeys"]
1319 if !ok {
gio11617ac2024-07-15 16:09:04 +04001320 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001321 }
1322 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
1323 if !ok {
gio11617ac2024-07-15 16:09:04 +04001324 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +04001325 }
1326 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001327 return err
gio33059762024-07-05 13:19:07 +04001328 } else if ok {
1329 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001330 return err
gio33059762024-07-05 13:19:07 +04001331 }
1332 } else {
1333 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +04001334 return err
gio33059762024-07-05 13:19:07 +04001335 }
1336 }
gio7fbd4ad2024-08-27 10:06:39 +04001337 if branch != "master" {
1338 return nil
1339 }
gio33059762024-07-05 13:19:07 +04001340 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +04001341 return err
gio33059762024-07-05 13:19:07 +04001342 }
gio7fbd4ad2024-08-27 10:06:39 +04001343 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +04001344 return err
gio33059762024-07-05 13:19:07 +04001345 }
gio7fbd4ad2024-08-27 10:06:39 +04001346 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 +04001347 return err
gio33059762024-07-05 13:19:07 +04001348 }
gio2ccb6e32024-08-15 12:01:33 +04001349 if !s.external {
1350 go func() {
1351 users, err := s.client.GetAllUsers()
1352 if err != nil {
1353 fmt.Println(err)
1354 return
1355 }
1356 for _, user := range users {
1357 // TODO(gio): fluxcd should have only read access
1358 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
1359 fmt.Println(err)
1360 }
1361 }
1362 }()
1363 }
gio43b0f422024-08-21 10:40:13 +04001364 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1365 go s.reconciler.Reconcile(ctx, s.namespace, "config")
gio11617ac2024-07-15 16:09:04 +04001366 return nil
gio33059762024-07-05 13:19:07 +04001367}
1368
gio81246f02024-07-10 12:02:15 +04001369type apiAddAdminKeyReq struct {
gio7fbd4ad2024-08-27 10:06:39 +04001370 User string `json:"user"`
1371 PublicKey string `json:"publicKey"`
gio70be3e52024-06-26 18:27:19 +04001372}
1373
gio59946282024-10-07 12:55:51 +04001374func (s *Server) handleAPIAddPublicKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +04001375 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +04001376 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
1377 http.Error(w, err.Error(), http.StatusBadRequest)
1378 return
1379 }
gio7fbd4ad2024-08-27 10:06:39 +04001380 if req.User == "" {
1381 http.Error(w, "invalid user", http.StatusBadRequest)
1382 return
1383 }
1384 if req.PublicKey == "" {
1385 http.Error(w, "invalid public key", http.StatusBadRequest)
1386 return
1387 }
1388 if err := s.client.AddPublicKey(req.User, req.PublicKey); err != nil {
gio70be3e52024-06-26 18:27:19 +04001389 http.Error(w, err.Error(), http.StatusInternalServerError)
1390 return
1391 }
1392}
1393
gio94904702024-07-26 16:58:34 +04001394type dodoAppRendered struct {
1395 App struct {
1396 Ingress struct {
1397 Network string `json:"network"`
1398 Subdomain string `json:"subdomain"`
1399 } `json:"ingress"`
1400 } `json:"app"`
1401 Input struct {
1402 AppId string `json:"appId"`
1403 } `json:"input"`
1404}
1405
gio7fbd4ad2024-08-27 10:06:39 +04001406// TODO(gio): must not require owner, now we need it to bootstrap dev vm.
gio59946282024-10-07 12:55:51 +04001407func (s *Server) updateDodoApp(
gio43b0f422024-08-21 10:40:13 +04001408 appStatus installer.EnvApp,
gio7fbd4ad2024-08-27 10:06:39 +04001409 name string,
1410 branch string,
1411 namespace string,
gio43b0f422024-08-21 10:40:13 +04001412 networks []installer.Network,
giof15b9da2024-09-19 06:59:16 +04001413 clusters []installer.Cluster,
gio7fbd4ad2024-08-27 10:06:39 +04001414 owner string,
gio43b0f422024-08-21 10:40:13 +04001415) (installer.ReleaseResources, error) {
gio7fbd4ad2024-08-27 10:06:39 +04001416 repo, err := s.client.GetRepoBranch(name, branch)
gio0eaf2712024-04-14 13:08:46 +04001417 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001418 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001419 }
giof8843412024-05-22 16:38:05 +04001420 hf := installer.NewGitHelmFetcher()
giof6ad2982024-08-23 17:42:49 +04001421 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +04001422 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001423 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001424 }
gioc81a8472024-09-24 13:06:19 +02001425 appCfg, err := soft.ReadFile(repo, "app.json")
gio0eaf2712024-04-14 13:08:46 +04001426 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001427 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001428 }
1429 app, err := installer.NewDodoApp(appCfg)
1430 if err != nil {
giob4a3a192024-08-19 09:55:47 +04001431 return installer.ReleaseResources{}, err
gio0eaf2712024-04-14 13:08:46 +04001432 }
giof8843412024-05-22 16:38:05 +04001433 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giob4a3a192024-08-19 09:55:47 +04001434 var ret installer.ReleaseResources
1435 if _, err := repo.Do(func(r soft.RepoFS) (string, error) {
1436 ret, err = m.Install(
gio94904702024-07-26 16:58:34 +04001437 app,
1438 "app",
1439 "/.dodo/app",
1440 namespace,
1441 map[string]any{
gio7fbd4ad2024-08-27 10:06:39 +04001442 "repoPublicAddr": s.repoPublicAddr,
1443 "managerAddr": fmt.Sprintf("http://%s", s.self),
1444 "appId": name,
1445 "branch": branch,
1446 "sshPrivateKey": s.sshKey,
1447 "username": owner,
gio94904702024-07-26 16:58:34 +04001448 },
1449 installer.WithNoPull(),
1450 installer.WithNoPublish(),
1451 installer.WithConfig(&s.env),
1452 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001453 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001454 installer.WithLocalChartGenerator(lg),
1455 installer.WithNoLock(),
1456 )
1457 if err != nil {
1458 return "", err
1459 }
1460 var rendered dodoAppRendered
giob4a3a192024-08-19 09:55:47 +04001461 if err := json.NewDecoder(bytes.NewReader(ret.RenderedRaw)).Decode(&rendered); err != nil {
gio94904702024-07-26 16:58:34 +04001462 return "", nil
1463 }
1464 if _, err := m.Install(
1465 appStatus,
1466 "status",
1467 "/.dodo/status",
1468 s.namespace,
1469 map[string]any{
1470 "appName": rendered.Input.AppId,
1471 "network": rendered.App.Ingress.Network,
1472 "appSubdomain": rendered.App.Ingress.Subdomain,
1473 },
1474 installer.WithNoPull(),
1475 installer.WithNoPublish(),
1476 installer.WithConfig(&s.env),
1477 installer.WithNetworks(networks),
giof15b9da2024-09-19 06:59:16 +04001478 installer.WithClusters(clusters),
gio94904702024-07-26 16:58:34 +04001479 installer.WithLocalChartGenerator(lg),
1480 installer.WithNoLock(),
1481 ); err != nil {
1482 return "", err
1483 }
1484 return "install app", nil
1485 },
gio7fbd4ad2024-08-27 10:06:39 +04001486 soft.WithCommitToBranch(fmt.Sprintf("dodo_%s", branch)),
gio94904702024-07-26 16:58:34 +04001487 soft.WithForce(),
giob4a3a192024-08-19 09:55:47 +04001488 ); err != nil {
1489 return installer.ReleaseResources{}, err
1490 }
gio43b0f422024-08-21 10:40:13 +04001491 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
1492 go s.reconciler.Reconcile(ctx, namespace, "app")
giob4a3a192024-08-19 09:55:47 +04001493 return ret, nil
gio0eaf2712024-04-14 13:08:46 +04001494}
gio33059762024-07-05 13:19:07 +04001495
gio9f6b27d2024-10-14 10:08:40 +04001496func (s *Server) renderAppConfigTemplate(user, appType string, network installer.Network, subdomain string) (map[string][]byte, error) {
giob54db242024-07-30 18:49:33 +04001497 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +04001498 appTmpl, err := s.appTmpls.Find(appType)
1499 if err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001500 return nil, err
gio33059762024-07-05 13:19:07 +04001501 }
gio9f6b27d2024-10-14 10:08:40 +04001502 return appTmpl.Render(fmt.Sprintf("%s/schemas/%s/app.schema.json", s.selfPublic, user), network, subdomain)
gio33059762024-07-05 13:19:07 +04001503}
gio81246f02024-07-10 12:02:15 +04001504
1505func generatePassword() string {
1506 return "foo"
1507}
giocb34ad22024-07-11 08:01:13 +04001508
gio59946282024-10-07 12:55:51 +04001509func (s *Server) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +04001510 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +04001511 resp, err := http.Get(addr)
1512 if err != nil {
1513 return nil, err
1514 }
gio23bdc1b2024-07-11 16:07:47 +04001515 networks := []installer.Network{}
1516 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +04001517 return nil, err
1518 }
gio11617ac2024-07-15 16:09:04 +04001519 return s.nf.Filter(user, networks)
1520}
1521
gio59946282024-10-07 12:55:51 +04001522func (s *Server) getClusters() ([]installer.Cluster, error) {
giof15b9da2024-09-19 06:59:16 +04001523 addr := fmt.Sprintf("%s/api/clusters", s.envAppManagerAddr)
1524 resp, err := http.Get(addr)
1525 if err != nil {
1526 return nil, err
1527 }
1528 clusters := []installer.Cluster{}
1529 if json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
1530 return nil, err
1531 }
1532 fmt.Printf("CLUSTERS %+v\n", clusters)
1533 return clusters, nil
1534}
1535
gio8fae3af2024-07-25 13:43:31 +04001536type publicNetworkData struct {
1537 Name string `json:"name"`
1538 Domain string `json:"domain"`
1539}
1540
1541type publicData struct {
1542 Networks []publicNetworkData `json:"networks"`
1543 Types []string `json:"types"`
1544}
1545
gio59946282024-10-07 12:55:51 +04001546func (s *Server) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +04001547 w.Header().Set("Access-Control-Allow-Origin", "*")
1548 s.l.Lock()
1549 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +04001550 networks, err := s.getNetworks("")
1551 if err != nil {
1552 http.Error(w, err.Error(), http.StatusInternalServerError)
1553 return
1554 }
1555 var ret publicData
1556 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +04001557 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
1558 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
1559 }
gio8fae3af2024-07-25 13:43:31 +04001560 }
1561 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +04001562 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +04001563 }
gio8fae3af2024-07-25 13:43:31 +04001564 if err := json.NewEncoder(w).Encode(ret); err != nil {
1565 http.Error(w, err.Error(), http.StatusInternalServerError)
1566 return
1567 }
1568}
1569
gio59946282024-10-07 12:55:51 +04001570func (s *Server) createCommit(name, branch, hash, message string, err error, resources installer.ReleaseResources) error {
giob4a3a192024-08-19 09:55:47 +04001571 if err != nil {
1572 fmt.Printf("Error: %s\n", err.Error())
gio7fbd4ad2024-08-27 10:06:39 +04001573 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001574 fmt.Printf("Error: %s\n", err.Error())
1575 return err
1576 }
1577 return err
1578 }
1579 var resB bytes.Buffer
1580 if err := json.NewEncoder(&resB).Encode(resources); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +04001581 if err := s.st.CreateCommit(name, branch, hash, message, "FAILED", err.Error(), nil); err != nil {
giob4a3a192024-08-19 09:55:47 +04001582 fmt.Printf("Error: %s\n", err.Error())
1583 return err
1584 }
1585 return err
1586 }
gio7fbd4ad2024-08-27 10:06:39 +04001587 if err := s.st.CreateCommit(name, branch, hash, message, "OK", "", resB.Bytes()); err != nil {
giob4a3a192024-08-19 09:55:47 +04001588 fmt.Printf("Error: %s\n", err.Error())
1589 return err
1590 }
1591 return nil
1592}
1593
gio11617ac2024-07-15 16:09:04 +04001594func pickNetwork(networks []installer.Network, network string) []installer.Network {
1595 for _, n := range networks {
1596 if n.Name == network {
1597 return []installer.Network{n}
1598 }
1599 }
1600 return []installer.Network{}
1601}
1602
1603type NetworkFilter interface {
1604 Filter(user string, networks []installer.Network) ([]installer.Network, error)
1605}
1606
1607type noNetworkFilter struct{}
1608
1609func NewNoNetworkFilter() NetworkFilter {
1610 return noNetworkFilter{}
1611}
1612
gio8fae3af2024-07-25 13:43:31 +04001613func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001614 return networks, nil
1615}
1616
1617type filterByOwner struct {
1618 st Store
1619}
1620
1621func NewNetworkFilterByOwner(st Store) NetworkFilter {
1622 return &filterByOwner{st}
1623}
1624
1625func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +04001626 if user == "" {
1627 return networks, nil
1628 }
gio11617ac2024-07-15 16:09:04 +04001629 network, err := f.st.GetUserNetwork(user)
1630 if err != nil {
1631 return nil, err
gio23bdc1b2024-07-11 16:07:47 +04001632 }
1633 ret := []installer.Network{}
1634 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +04001635 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +04001636 ret = append(ret, n)
1637 }
1638 }
giocb34ad22024-07-11 08:01:13 +04001639 return ret, nil
1640}
gio11617ac2024-07-15 16:09:04 +04001641
1642type allowListFilter struct {
1643 allowed []string
1644}
1645
1646func NewAllowListFilter(allowed []string) NetworkFilter {
1647 return &allowListFilter{allowed}
1648}
1649
gio8fae3af2024-07-25 13:43:31 +04001650func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001651 ret := []installer.Network{}
1652 for _, n := range networks {
1653 if slices.Contains(f.allowed, n.Name) {
1654 ret = append(ret, n)
1655 }
1656 }
1657 return ret, nil
1658}
1659
1660type combinedNetworkFilter struct {
1661 filters []NetworkFilter
1662}
1663
1664func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1665 return &combinedNetworkFilter{filters}
1666}
1667
gio8fae3af2024-07-25 13:43:31 +04001668func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001669 ret := networks
1670 var err error
1671 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001672 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001673 if err != nil {
1674 return nil, err
1675 }
1676 }
1677 return ret, nil
1678}
giocafd4e62024-07-31 10:53:40 +04001679
1680type user struct {
1681 Username string `json:"username"`
1682 Email string `json:"email"`
1683 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
1684}
1685
gio59946282024-10-07 12:55:51 +04001686func (s *Server) handleAPISyncUsers(_ http.ResponseWriter, _ *http.Request) {
giocafd4e62024-07-31 10:53:40 +04001687 go s.syncUsers()
1688}
1689
gio59946282024-10-07 12:55:51 +04001690func (s *Server) syncUsers() {
giocafd4e62024-07-31 10:53:40 +04001691 if s.external {
1692 panic("MUST NOT REACH!")
1693 }
1694 resp, err := http.Get(fmt.Sprintf("%s?selfAddress=%s/api/sync-users", s.fetchUsersAddr, s.self))
1695 if err != nil {
1696 return
1697 }
1698 users := []user{}
1699 if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
1700 fmt.Println(err)
1701 return
1702 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001703 validUsernames := make(map[string]user)
1704 for _, u := range users {
1705 validUsernames[u.Username] = u
1706 }
1707 allClientUsers, err := s.client.GetAllUsers()
1708 if err != nil {
1709 fmt.Println(err)
1710 return
1711 }
1712 keyToUser := make(map[string]string)
1713 for _, clientUser := range allClientUsers {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001714 if clientUser == "admin" || clientUser == "fluxcd" {
1715 continue
1716 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001717 userData, ok := validUsernames[clientUser]
1718 if !ok {
1719 if err := s.client.RemoveUser(clientUser); err != nil {
1720 fmt.Println(err)
1721 return
1722 }
1723 } else {
1724 existingKeys, err := s.client.GetUserPublicKeys(clientUser)
1725 if err != nil {
1726 fmt.Println(err)
1727 return
1728 }
1729 for _, existingKey := range existingKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001730 cleanKey := soft.CleanKey(existingKey)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001731 keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001732 return cleanKey == soft.CleanKey(key)
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001733 })
1734 if !keyOk {
1735 if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
1736 fmt.Println(err)
1737 }
1738 } else {
1739 keyToUser[cleanKey] = clientUser
1740 }
1741 }
1742 }
1743 }
giocafd4e62024-07-31 10:53:40 +04001744 for _, u := range users {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001745 if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
1746 fmt.Println(err)
1747 return
1748 }
giocafd4e62024-07-31 10:53:40 +04001749 if len(u.SSHPublicKeys) == 0 {
1750 continue
1751 }
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001752 ok, err := s.client.UserExists(u.Username)
1753 if err != nil {
giocafd4e62024-07-31 10:53:40 +04001754 fmt.Println(err)
1755 return
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001756 }
1757 if !ok {
1758 if err := s.client.AddUser(u.Username, u.SSHPublicKeys[0]); err != nil {
1759 fmt.Println(err)
1760 return
1761 }
1762 } else {
1763 for _, key := range u.SSHPublicKeys {
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001764 cleanKey := soft.CleanKey(key)
1765 if user, ok := keyToUser[cleanKey]; ok {
1766 if u.Username != user {
1767 panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
1768 }
1769 continue
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001770 }
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001771 if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
Davit Tabidzea5ea5092024-08-01 15:28:09 +04001772 fmt.Println(err)
1773 return
giocafd4e62024-07-31 10:53:40 +04001774 }
1775 }
1776 }
1777 }
1778 repos, err := s.client.GetAllRepos()
1779 if err != nil {
1780 return
1781 }
1782 for _, r := range repos {
1783 if r == ConfigRepoName {
1784 continue
1785 }
1786 for _, u := range users {
1787 if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
1788 fmt.Println(err)
Davit Tabidze4aaa27b2024-08-05 20:23:50 +04001789 continue
giocafd4e62024-07-31 10:53:40 +04001790 }
1791 }
1792 }
1793}
giob4a3a192024-08-19 09:55:47 +04001794
gio85958d62024-10-26 09:14:01 +04001795type lastCmdEnv struct {
1796 App struct {
1797 Env []string `json:"lastCmdEnv"`
1798 } `json:"app"`
1799}
1800
1801func extractResourceData(resources installer.ReleaseResources) (resourceData, error) {
giob4a3a192024-08-19 09:55:47 +04001802 var ret resourceData
gio85958d62024-10-26 09:14:01 +04001803 var raw lastCmdEnv
1804 if err := json.NewDecoder(bytes.NewReader(resources.RenderedRaw)).Decode(&raw); err != nil {
1805 return resourceData{}, err
1806 }
1807 ret.Env = raw.App.Env
1808 for _, r := range resources.Helm {
giob4a3a192024-08-19 09:55:47 +04001809 t, ok := r.Annotations["dodo.cloud/resource-type"]
1810 if !ok {
1811 continue
1812 }
giof078f462024-10-14 09:07:33 +04001813 internal, ok := r.Annotations["dodo.cloud/internal"]
1814 if ok && strings.ToLower(internal) == "true" {
1815 continue
1816 }
giob4a3a192024-08-19 09:55:47 +04001817 switch t {
1818 case "volume":
1819 name, ok := r.Annotations["dodo.cloud/resource.volume.name"]
1820 if !ok {
1821 return resourceData{}, fmt.Errorf("no name")
1822 }
1823 size, ok := r.Annotations["dodo.cloud/resource.volume.size"]
1824 if !ok {
1825 return resourceData{}, fmt.Errorf("no size")
1826 }
1827 ret.Volume = append(ret.Volume, volume{name, size})
1828 case "postgresql":
1829 name, ok := r.Annotations["dodo.cloud/resource.postgresql.name"]
1830 if !ok {
1831 return resourceData{}, fmt.Errorf("no name")
1832 }
1833 version, ok := r.Annotations["dodo.cloud/resource.postgresql.version"]
1834 if !ok {
1835 return resourceData{}, fmt.Errorf("no version")
1836 }
1837 volume, ok := r.Annotations["dodo.cloud/resource.postgresql.volume"]
1838 if !ok {
1839 return resourceData{}, fmt.Errorf("no volume")
1840 }
1841 ret.PostgreSQL = append(ret.PostgreSQL, postgresql{name, version, volume})
gio07eb1082024-10-25 14:35:56 +04001842 case "mongodb":
1843 name, ok := r.Annotations["dodo.cloud/resource.mongodb.name"]
1844 if !ok {
1845 return resourceData{}, fmt.Errorf("no name")
1846 }
1847 version, ok := r.Annotations["dodo.cloud/resource.mongodb.version"]
1848 if !ok {
1849 return resourceData{}, fmt.Errorf("no version")
1850 }
1851 volume, ok := r.Annotations["dodo.cloud/resource.mongodb.volume"]
1852 if !ok {
1853 return resourceData{}, fmt.Errorf("no volume")
1854 }
1855 ret.MongoDB = append(ret.MongoDB, mongodb{name, version, volume})
giob4a3a192024-08-19 09:55:47 +04001856 case "ingress":
giof078f462024-10-14 09:07:33 +04001857 name, ok := r.Annotations["dodo.cloud/resource.ingress.name"]
1858 if !ok {
1859 return resourceData{}, fmt.Errorf("no name")
1860 }
1861 home, ok := r.Annotations["dodo.cloud/resource.ingress.home"]
1862 if !ok {
1863 home = ""
1864 }
giob4a3a192024-08-19 09:55:47 +04001865 host, ok := r.Annotations["dodo.cloud/resource.ingress.host"]
1866 if !ok {
1867 return resourceData{}, fmt.Errorf("no host")
1868 }
giof078f462024-10-14 09:07:33 +04001869 ret.Ingress = append(ret.Ingress, ingress{name, host, home})
gio7fbd4ad2024-08-27 10:06:39 +04001870 case "virtual-machine":
1871 name, ok := r.Annotations["dodo.cloud/resource.virtual-machine.name"]
1872 if !ok {
1873 return resourceData{}, fmt.Errorf("no name")
1874 }
1875 user, ok := r.Annotations["dodo.cloud/resource.virtual-machine.user"]
1876 if !ok {
1877 return resourceData{}, fmt.Errorf("no user")
1878 }
1879 cpuCoresS, ok := r.Annotations["dodo.cloud/resource.virtual-machine.cpu-cores"]
1880 if !ok {
1881 return resourceData{}, fmt.Errorf("no cpu cores")
1882 }
1883 cpuCores, err := strconv.Atoi(cpuCoresS)
1884 if err != nil {
1885 return resourceData{}, fmt.Errorf("invalid cpu cores: %s", cpuCoresS)
1886 }
1887 memory, ok := r.Annotations["dodo.cloud/resource.virtual-machine.memory"]
1888 if !ok {
1889 return resourceData{}, fmt.Errorf("no memory")
1890 }
1891 ret.VirtualMachine = append(ret.VirtualMachine, vm{name, user, cpuCores, memory})
giob4a3a192024-08-19 09:55:47 +04001892 default:
1893 fmt.Printf("Unknown resource: %+v\n", r.Annotations)
1894 }
1895 }
giof078f462024-10-14 09:07:33 +04001896 sort.Slice(ret.Ingress, func(i, j int) bool {
1897 return strings.Compare(ret.Ingress[i].Name, ret.Ingress[j].Name) < 0
1898 })
giob4a3a192024-08-19 09:55:47 +04001899 return ret, nil
1900}
gio7fbd4ad2024-08-27 10:06:39 +04001901
gio9f6b27d2024-10-14 10:08:40 +04001902func createDevBranchAppConfig(from []byte, branch, username, publicAddr string) (string, []byte, error) {
gioc81a8472024-09-24 13:06:19 +02001903 cfg, err := installer.ParseCueAppConfig(installer.CueAppData{
1904 "app.cue": from,
1905 })
gio7fbd4ad2024-08-27 10:06:39 +04001906 if err != nil {
1907 return "", nil, err
1908 }
1909 if err := cfg.Err(); err != nil {
1910 return "", nil, err
1911 }
1912 if err := cfg.Validate(); err != nil {
1913 return "", nil, err
1914 }
1915 subdomain := cfg.LookupPath(cue.ParsePath("app.ingress.subdomain"))
1916 if err := subdomain.Err(); err != nil {
1917 return "", nil, err
1918 }
1919 subdomainStr, err := subdomain.String()
1920 network := cfg.LookupPath(cue.ParsePath("app.ingress.network"))
1921 if err := network.Err(); err != nil {
1922 return "", nil, err
1923 }
1924 networkStr, err := network.String()
1925 if err != nil {
1926 return "", nil, err
1927 }
1928 newCfg := map[string]any{}
1929 if err := cfg.Decode(&newCfg); err != nil {
1930 return "", nil, err
1931 }
1932 app, ok := newCfg["app"].(map[string]any)
1933 if !ok {
1934 return "", nil, fmt.Errorf("not a map")
1935 }
1936 app["ingress"].(map[string]any)["subdomain"] = fmt.Sprintf("%s-%s", branch, subdomainStr)
1937 app["dev"] = map[string]any{
1938 "enabled": true,
1939 "username": username,
1940 }
gio9f6b27d2024-10-14 10:08:40 +04001941 newCfg["$schema"] = fmt.Sprintf("%s/schemas/%s/app.schema.json", publicAddr, username)
gio7fbd4ad2024-08-27 10:06:39 +04001942 buf, err := json.MarshalIndent(newCfg, "", "\t")
1943 if err != nil {
1944 return "", nil, err
1945 }
1946 return networkStr, buf, nil
1947}