blob: 20e881d7ff27824c133b34f1db00d7b93c847772 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
gio81246f02024-07-10 12:02:15 +04004 "context"
gio0eaf2712024-04-14 13:08:46 +04005 "encoding/json"
gio9d66f322024-07-06 13:45:10 +04006 "errors"
gio0eaf2712024-04-14 13:08:46 +04007 "fmt"
gio81246f02024-07-10 12:02:15 +04008 "golang.org/x/crypto/bcrypt"
gio0eaf2712024-04-14 13:08:46 +04009 "io"
gio9d66f322024-07-06 13:45:10 +040010 "io/fs"
gio0eaf2712024-04-14 13:08:46 +040011 "net/http"
12 "strings"
gio9d66f322024-07-06 13:45:10 +040013 "sync"
gio0eaf2712024-04-14 13:08:46 +040014
15 "github.com/giolekva/pcloud/core/installer"
16 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040017
18 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040019 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040020)
21
gio9d66f322024-07-06 13:45:10 +040022const (
gioa60f0de2024-07-08 10:49:48 +040023 ConfigRepoName = "config"
gio9d66f322024-07-06 13:45:10 +040024 namespacesFile = "/namespaces.json"
gio81246f02024-07-10 12:02:15 +040025 loginPath = "/login"
26 logoutPath = "/logout"
27 sessionCookie = "dodo-app-session"
28 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040029)
30
gio0eaf2712024-04-14 13:08:46 +040031type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040032 l sync.Locker
33 st Store
34 port int
35 apiPort int
36 self string
37 sshKey string
38 gitRepoPublicKey string
39 client soft.Client
40 namespace string
41 envAppManagerAddr string
42 env installer.EnvConfig
43 nsc installer.NamespaceCreator
44 jc installer.JobCreator
45 workers map[string]map[string]struct{}
46 appNs map[string]string
47 sc *securecookie.SecureCookie
gio0eaf2712024-04-14 13:08:46 +040048}
49
gio33059762024-07-05 13:19:07 +040050// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040051func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +040052 st Store,
gio0eaf2712024-04-14 13:08:46 +040053 port int,
gioa60f0de2024-07-08 10:49:48 +040054 apiPort int,
gio33059762024-07-05 13:19:07 +040055 self string,
gio0eaf2712024-04-14 13:08:46 +040056 sshKey string,
gio33059762024-07-05 13:19:07 +040057 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +040058 client soft.Client,
59 namespace string,
giocb34ad22024-07-11 08:01:13 +040060 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +040061 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040062 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +040063 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +040064) (*DodoAppServer, error) {
gio81246f02024-07-10 12:02:15 +040065 sc := securecookie.New(
66 securecookie.GenerateRandomKey(64),
67 securecookie.GenerateRandomKey(32),
68 )
gio9d66f322024-07-06 13:45:10 +040069 s := &DodoAppServer{
70 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +040071 st,
gio0eaf2712024-04-14 13:08:46 +040072 port,
gioa60f0de2024-07-08 10:49:48 +040073 apiPort,
gio33059762024-07-05 13:19:07 +040074 self,
gio0eaf2712024-04-14 13:08:46 +040075 sshKey,
gio33059762024-07-05 13:19:07 +040076 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +040077 client,
78 namespace,
giocb34ad22024-07-11 08:01:13 +040079 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +040080 env,
gio33059762024-07-05 13:19:07 +040081 nsc,
giof8843412024-05-22 16:38:05 +040082 jc,
gio266c04f2024-07-03 14:18:45 +040083 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +040084 map[string]string{},
gio81246f02024-07-10 12:02:15 +040085 sc,
gio0eaf2712024-04-14 13:08:46 +040086 }
gioa60f0de2024-07-08 10:49:48 +040087 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +040088 if err != nil {
89 return nil, err
90 }
91 r, err := config.Reader(namespacesFile)
92 if err == nil {
93 defer r.Close()
94 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
95 return nil, err
96 }
97 } else if !errors.Is(err, fs.ErrNotExist) {
98 return nil, err
99 }
100 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400101}
102
103func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400104 e := make(chan error)
105 go func() {
106 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400107 r.Use(s.mwAuth)
108 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
109 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
110 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
111 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
112 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gioa60f0de2024-07-08 10:49:48 +0400113 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
114 }()
115 go func() {
116 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400117 r.HandleFunc("/update", s.handleApiUpdate)
118 r.HandleFunc("/api/apps/{app-name}/workers", s.handleApiRegisterWorker).Methods(http.MethodPost)
119 r.HandleFunc("/api/apps", s.handleApiCreateApp).Methods(http.MethodPost)
120 r.HandleFunc("/api/add-admin-key", s.handleApiAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400121 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
122 }()
123 return <-e
124}
125
gio81246f02024-07-10 12:02:15 +0400126func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
127 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128 if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) {
129 next.ServeHTTP(w, r)
130 return
131 }
132 cookie, err := r.Cookie(sessionCookie)
133 if err != nil {
134 vars := mux.Vars(r)
135 appName, ok := vars["app-name"]
136 if !ok || appName == "" {
137 http.Error(w, "missing app-name", http.StatusBadRequest)
138 return
139 }
140 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
141 return
142 }
143 var user string
144 if err := s.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
145 http.Error(w, "unauthorized", http.StatusUnauthorized)
146 return
147 }
148 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
149 })
150}
151
152func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
153 http.SetCookie(w, &http.Cookie{
154 Name: sessionCookie,
155 Value: "",
156 Path: "/",
157 HttpOnly: true,
158 Secure: true,
159 })
160 http.Redirect(w, r, "/", http.StatusSeeOther)
161}
162
163func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
164 vars := mux.Vars(r)
165 appName, ok := vars["app-name"]
166 if !ok || appName == "" {
167 http.Error(w, "missing app-name", http.StatusBadRequest)
168 return
169 }
170 fmt.Fprint(w, `
171<!DOCTYPE html>
172<html lang='en'>
173 <head>
174 <title>dodo: app - login</title>
175 <meta charset='utf-8'>
176 </head>
177 <body>
178 <form action="" method="POST">
179 <input type="password" placeholder="Password" name="password" required />
180 <button type="submit">Login</button>
181 </form>
182 </body>
183</html>
184`)
185}
186
187func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
188 vars := mux.Vars(r)
189 appName, ok := vars["app-name"]
190 if !ok || appName == "" {
191 http.Error(w, "missing app-name", http.StatusBadRequest)
192 return
193 }
194 password := r.FormValue("password")
195 if password == "" {
196 http.Error(w, "missing password", http.StatusBadRequest)
197 return
198 }
199 user, err := s.st.GetAppOwner(appName)
200 if err != nil {
201 http.Error(w, err.Error(), http.StatusInternalServerError)
202 return
203 }
204 hashed, err := s.st.GetUserPassword(user)
205 if err != nil {
206 http.Error(w, err.Error(), http.StatusInternalServerError)
207 return
208 }
209 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
210 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
211 return
212 }
213 if encoded, err := s.sc.Encode(sessionCookie, user); err == nil {
214 cookie := &http.Cookie{
215 Name: sessionCookie,
216 Value: encoded,
217 Path: "/",
218 Secure: true,
219 HttpOnly: true,
220 }
221 http.SetCookie(w, cookie)
222 }
223 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
224}
225
gioa60f0de2024-07-08 10:49:48 +0400226func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400227 user := r.Context().Value(userCtx)
228 if user == nil {
229 http.Error(w, "unauthorized", http.StatusUnauthorized)
230 return
231 }
232 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400233 if err != nil {
234 http.Error(w, err.Error(), http.StatusInternalServerError)
235 return
236 }
237 for _, a := range apps {
238 fmt.Fprintf(w, "%s\n", a)
239 }
240}
241
242func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
243 vars := mux.Vars(r)
244 appName, ok := vars["app-name"]
245 if !ok || appName == "" {
246 http.Error(w, "missing app-name", http.StatusBadRequest)
247 return
248 }
249 commits, err := s.st.GetCommitHistory(appName)
250 if err != nil {
251 http.Error(w, err.Error(), http.StatusInternalServerError)
252 return
253 }
254 for _, c := range commits {
255 fmt.Fprintf(w, "%s %s\n", c.Hash, c.Message)
256 }
gio0eaf2712024-04-14 13:08:46 +0400257}
258
gio81246f02024-07-10 12:02:15 +0400259type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400260 Ref string `json:"ref"`
261 Repository struct {
262 Name string `json:"name"`
263 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400264 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400265}
266
gio81246f02024-07-10 12:02:15 +0400267func (s *DodoAppServer) handleApiUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400268 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400269 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400270 var contents strings.Builder
271 io.Copy(&contents, r.Body)
272 c := contents.String()
273 fmt.Println(c)
274 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
275 fmt.Println(err)
276 return
277 }
gioa60f0de2024-07-08 10:49:48 +0400278 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400279 return
280 }
gioa60f0de2024-07-08 10:49:48 +0400281 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400282 go func() {
giocb34ad22024-07-11 08:01:13 +0400283 networks, err := getNetworks(fmt.Sprintf("%s/api/networks", s.envAppManagerAddr))
284 if err != nil {
285 return
286 }
287 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400288 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
289 fmt.Printf("Error: %s\n", err.Error())
290 return
291 }
292 }
293 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
294 fmt.Printf("Error: %s\n", err.Error())
295 }
296 for addr, _ := range s.workers[req.Repository.Name] {
297 go func() {
298 // TODO(gio): make port configurable
299 http.Get(fmt.Sprintf("http://%s/update", addr))
300 }()
gio0eaf2712024-04-14 13:08:46 +0400301 }
302 }()
gio0eaf2712024-04-14 13:08:46 +0400303}
304
gio81246f02024-07-10 12:02:15 +0400305type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400306 Address string `json:"address"`
307}
308
gio81246f02024-07-10 12:02:15 +0400309func (s *DodoAppServer) handleApiRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400310 vars := mux.Vars(r)
311 appName, ok := vars["app-name"]
312 if !ok || appName == "" {
313 http.Error(w, "missing app-name", http.StatusBadRequest)
314 return
315 }
gio81246f02024-07-10 12:02:15 +0400316 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400317 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
320 }
gioa60f0de2024-07-08 10:49:48 +0400321 if _, ok := s.workers[appName]; !ok {
322 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400323 }
gioa60f0de2024-07-08 10:49:48 +0400324 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400325}
326
gio81246f02024-07-10 12:02:15 +0400327type apiCreateAppReq struct {
gio33059762024-07-05 13:19:07 +0400328 AdminPublicKey string `json:"adminPublicKey"`
329}
330
gio81246f02024-07-10 12:02:15 +0400331type apiCreateAppResp struct {
332 AppName string `json:"appName"`
333 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400334}
335
gio81246f02024-07-10 12:02:15 +0400336func (s *DodoAppServer) handleApiCreateApp(w http.ResponseWriter, r *http.Request) {
337 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400338 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
339 http.Error(w, err.Error(), http.StatusBadRequest)
340 return
341 }
342 g := installer.NewFixedLengthRandomNameGenerator(3)
343 appName, err := g.Generate()
344 if err != nil {
345 http.Error(w, err.Error(), http.StatusInternalServerError)
346 return
347 }
gio81246f02024-07-10 12:02:15 +0400348 password, err := s.CreateApp(appName, req.AdminPublicKey)
349 if err != nil {
gio33059762024-07-05 13:19:07 +0400350 http.Error(w, err.Error(), http.StatusInternalServerError)
351 return
352 }
gio81246f02024-07-10 12:02:15 +0400353 resp := apiCreateAppResp{
354 AppName: appName,
355 Password: password,
356 }
gio33059762024-07-05 13:19:07 +0400357 if err := json.NewEncoder(w).Encode(resp); err != nil {
358 http.Error(w, err.Error(), http.StatusInternalServerError)
359 return
360 }
361}
362
gio81246f02024-07-10 12:02:15 +0400363func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) (string, error) {
gio9d66f322024-07-06 13:45:10 +0400364 s.l.Lock()
365 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400366 fmt.Printf("Creating app: %s\n", appName)
367 if ok, err := s.client.RepoExists(appName); err != nil {
gio81246f02024-07-10 12:02:15 +0400368 return "", err
gio33059762024-07-05 13:19:07 +0400369 } else if ok {
gio81246f02024-07-10 12:02:15 +0400370 return "", nil
gio33059762024-07-05 13:19:07 +0400371 }
gio81246f02024-07-10 12:02:15 +0400372 user, err := s.client.FindUser(adminPublicKey)
373 if err != nil {
374 return "", err
375 }
376 if user != "" {
377 if err := s.client.AddPublicKey(user, adminPublicKey); err != nil {
378 return "", err
379 }
380 } else {
381 user = appName
382 if err := s.client.AddUser(user, adminPublicKey); err != nil {
383 return "", err
384 }
385 }
386 password := generatePassword()
387 // TODO(gio): take admin password for initial application as input
388 if appName == "app" {
389 password = "app"
390 }
391 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
392 if err != nil {
393 return "", err
394 }
395 if err := s.st.CreateUser(user, hashed); err != nil {
396 if !errors.Is(err, ErrorAlreadyExists) {
397 return "", err
398 } else {
399 password = ""
400 }
401 }
402 if err := s.st.CreateApp(appName, user); err != nil {
403 return "", err
gioa60f0de2024-07-08 10:49:48 +0400404 }
gio33059762024-07-05 13:19:07 +0400405 if err := s.client.AddRepository(appName); err != nil {
gio81246f02024-07-10 12:02:15 +0400406 return "", err
gio33059762024-07-05 13:19:07 +0400407 }
408 appRepo, err := s.client.GetRepo(appName)
409 if err != nil {
gio81246f02024-07-10 12:02:15 +0400410 return "", err
gio33059762024-07-05 13:19:07 +0400411 }
412 if err := InitRepo(appRepo); err != nil {
gio81246f02024-07-10 12:02:15 +0400413 return "", err
gio33059762024-07-05 13:19:07 +0400414 }
415 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
416 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
417 if err != nil {
gio81246f02024-07-10 12:02:15 +0400418 return "", err
gio33059762024-07-05 13:19:07 +0400419 }
420 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
421 suffix, err := suffixGen.Generate()
422 if err != nil {
gio81246f02024-07-10 12:02:15 +0400423 return "", err
gio33059762024-07-05 13:19:07 +0400424 }
425 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
426 s.appNs[appName] = namespace
giocb34ad22024-07-11 08:01:13 +0400427 networks, err := getNetworks(fmt.Sprintf("%s/api/networks", s.envAppManagerAddr))
428 if err != nil {
429 return "", err
430 }
431 if err := s.updateDodoApp(appName, namespace, networks); err != nil {
gio81246f02024-07-10 12:02:15 +0400432 return "", err
gio33059762024-07-05 13:19:07 +0400433 }
gioa60f0de2024-07-08 10:49:48 +0400434 repo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400435 if err != nil {
gio81246f02024-07-10 12:02:15 +0400436 return "", err
gio33059762024-07-05 13:19:07 +0400437 }
438 hf := installer.NewGitHelmFetcher()
439 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
440 if err != nil {
gio81246f02024-07-10 12:02:15 +0400441 return "", err
gio33059762024-07-05 13:19:07 +0400442 }
gio9d66f322024-07-06 13:45:10 +0400443 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
444 w, err := fs.Writer(namespacesFile)
445 if err != nil {
446 return "", err
447 }
448 defer w.Close()
449 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
450 return "", err
451 }
452 if _, err := m.Install(
453 app,
454 appName,
455 "/"+appName,
456 namespace,
457 map[string]any{
458 "repoAddr": s.client.GetRepoAddress(appName),
459 "repoHost": strings.Split(s.client.Address(), ":")[0],
460 "gitRepoPublicKey": s.gitRepoPublicKey,
461 },
462 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400463 installer.WithNetworks(networks),
gio9d66f322024-07-06 13:45:10 +0400464 installer.WithNoPublish(),
465 installer.WithNoLock(),
466 ); err != nil {
467 return "", err
468 }
469 return fmt.Sprintf("Installed app: %s", appName), nil
470 }); err != nil {
gio81246f02024-07-10 12:02:15 +0400471 return "", err
gio33059762024-07-05 13:19:07 +0400472 }
473 cfg, err := m.FindInstance(appName)
474 if err != nil {
gio81246f02024-07-10 12:02:15 +0400475 return "", err
gio33059762024-07-05 13:19:07 +0400476 }
477 fluxKeys, ok := cfg.Input["fluxKeys"]
478 if !ok {
gio81246f02024-07-10 12:02:15 +0400479 return "", fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400480 }
481 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
482 if !ok {
gio81246f02024-07-10 12:02:15 +0400483 return "", fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400484 }
485 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio81246f02024-07-10 12:02:15 +0400486 return "", err
gio33059762024-07-05 13:19:07 +0400487 } else if ok {
488 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio81246f02024-07-10 12:02:15 +0400489 return "", err
gio33059762024-07-05 13:19:07 +0400490 }
491 } else {
492 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio81246f02024-07-10 12:02:15 +0400493 return "", err
gio33059762024-07-05 13:19:07 +0400494 }
495 }
496 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio81246f02024-07-10 12:02:15 +0400497 return "", err
gio33059762024-07-05 13:19:07 +0400498 }
499 if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
gio81246f02024-07-10 12:02:15 +0400500 return "", err
gio33059762024-07-05 13:19:07 +0400501 }
gio81246f02024-07-10 12:02:15 +0400502 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
503 return "", err
gio33059762024-07-05 13:19:07 +0400504 }
gio81246f02024-07-10 12:02:15 +0400505 return password, nil
gio33059762024-07-05 13:19:07 +0400506}
507
gio81246f02024-07-10 12:02:15 +0400508type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400509 Public string `json:"public"`
510}
511
gio81246f02024-07-10 12:02:15 +0400512func (s *DodoAppServer) handleApiAddAdminKey(w http.ResponseWriter, r *http.Request) {
513 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400514 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
515 http.Error(w, err.Error(), http.StatusBadRequest)
516 return
517 }
518 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
519 http.Error(w, err.Error(), http.StatusInternalServerError)
520 return
521 }
522}
523
giocb34ad22024-07-11 08:01:13 +0400524func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400525 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400526 if err != nil {
527 return err
528 }
giof8843412024-05-22 16:38:05 +0400529 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400530 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400531 if err != nil {
532 return err
533 }
534 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400535 if err != nil {
536 return err
537 }
538 app, err := installer.NewDodoApp(appCfg)
539 if err != nil {
540 return err
541 }
giof8843412024-05-22 16:38:05 +0400542 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400543 if _, err := m.Install(
544 app,
545 "app",
546 "/.dodo/app",
547 namespace,
548 map[string]any{
gioa60f0de2024-07-08 10:49:48 +0400549 "repoAddr": repo.FullAddress(),
550 "managerAddr": fmt.Sprintf("http://%s", s.self),
551 "appId": name,
552 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400553 },
gio33059762024-07-05 13:19:07 +0400554 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400555 installer.WithNetworks(networks),
giof71a0832024-06-27 14:45:45 +0400556 installer.WithLocalChartGenerator(lg),
557 installer.WithBranch("dodo"),
558 installer.WithForce(),
559 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400560 return err
561 }
562 return nil
563}
gio33059762024-07-05 13:19:07 +0400564
565const goMod = `module dodo.app
566
567go 1.18
568`
569
570const mainGo = `package main
571
572import (
573 "flag"
574 "fmt"
575 "log"
576 "net/http"
577)
578
579var port = flag.Int("port", 8080, "Port to listen on")
580
581func handler(w http.ResponseWriter, r *http.Request) {
582 fmt.Fprintln(w, "Hello from Dodo App!")
583}
584
585func main() {
586 flag.Parse()
587 http.HandleFunc("/", handler)
588 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
589}
590`
591
592const appCue = `app: {
593 type: "golang:1.22.0"
594 run: "main.go"
595 ingress: {
596 network: "Private" // or Public
597 subdomain: "testapp"
598 auth: enabled: false
599 }
600}
601`
602
603func InitRepo(repo soft.RepoIO) error {
604 return repo.Do(func(fs soft.RepoFS) (string, error) {
605 {
606 w, err := fs.Writer("go.mod")
607 if err != nil {
608 return "", err
609 }
610 defer w.Close()
611 fmt.Fprint(w, goMod)
612 }
613 {
614 w, err := fs.Writer("main.go")
615 if err != nil {
616 return "", err
617 }
618 defer w.Close()
619 fmt.Fprintf(w, "%s", mainGo)
620 }
621 {
622 w, err := fs.Writer("app.cue")
623 if err != nil {
624 return "", err
625 }
626 defer w.Close()
627 fmt.Fprint(w, appCue)
628 }
629 return "go web app template", nil
630 })
631}
gio81246f02024-07-10 12:02:15 +0400632
633func generatePassword() string {
634 return "foo"
635}
giocb34ad22024-07-11 08:01:13 +0400636
637func getNetworks(addr string) ([]installer.Network, error) {
638 resp, err := http.Get(addr)
639 if err != nil {
640 return nil, err
641 }
642 ret := []installer.Network{}
643 if json.NewDecoder(resp.Body).Decode(&ret); err != nil {
644 return nil, err
645 }
646 return ret, nil
647}