blob: 1c91cbc41e1736abf85d474415df6266baf1f164 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
gio81246f02024-07-10 12:02:15 +04004 "context"
gio23bdc1b2024-07-11 16:07:47 +04005 "embed"
gio0eaf2712024-04-14 13:08:46 +04006 "encoding/json"
gio9d66f322024-07-06 13:45:10 +04007 "errors"
gio0eaf2712024-04-14 13:08:46 +04008 "fmt"
gio81246f02024-07-10 12:02:15 +04009 "golang.org/x/crypto/bcrypt"
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"
gio0eaf2712024-04-14 13:08:46 +040015 "strings"
gio9d66f322024-07-06 13:45:10 +040016 "sync"
gio0eaf2712024-04-14 13:08:46 +040017
18 "github.com/giolekva/pcloud/core/installer"
19 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040020
21 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040022 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040023)
24
gio23bdc1b2024-07-11 16:07:47 +040025//go:embed dodo-app-tmpl/*
26var dodoAppTmplFS embed.FS
27
gio9d66f322024-07-06 13:45:10 +040028const (
gioa60f0de2024-07-08 10:49:48 +040029 ConfigRepoName = "config"
gio9d66f322024-07-06 13:45:10 +040030 namespacesFile = "/namespaces.json"
gio81246f02024-07-10 12:02:15 +040031 loginPath = "/login"
32 logoutPath = "/logout"
33 sessionCookie = "dodo-app-session"
34 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040035)
36
gio23bdc1b2024-07-11 16:07:47 +040037type dodoAppTmplts struct {
38 index *template.Template
39}
40
41func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
42 index, err := template.New("index.html").ParseFS(fs, "dodo-app-tmpl/index.html")
43 if err != nil {
44 return dodoAppTmplts{}, err
45 }
46 return dodoAppTmplts{index}, nil
47}
48
gio0eaf2712024-04-14 13:08:46 +040049type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040050 l sync.Locker
51 st Store
52 port int
53 apiPort int
54 self string
55 sshKey string
56 gitRepoPublicKey string
57 client soft.Client
58 namespace string
59 envAppManagerAddr string
gio23bdc1b2024-07-11 16:07:47 +040060 networks []string
giocb34ad22024-07-11 08:01:13 +040061 env installer.EnvConfig
62 nsc installer.NamespaceCreator
63 jc installer.JobCreator
64 workers map[string]map[string]struct{}
65 appNs map[string]string
66 sc *securecookie.SecureCookie
gio23bdc1b2024-07-11 16:07:47 +040067 tmplts dodoAppTmplts
gio0eaf2712024-04-14 13:08:46 +040068}
69
gio33059762024-07-05 13:19:07 +040070// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040071func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +040072 st Store,
gio0eaf2712024-04-14 13:08:46 +040073 port int,
gioa60f0de2024-07-08 10:49:48 +040074 apiPort int,
gio33059762024-07-05 13:19:07 +040075 self string,
gio0eaf2712024-04-14 13:08:46 +040076 sshKey string,
gio33059762024-07-05 13:19:07 +040077 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +040078 client soft.Client,
79 namespace string,
giocb34ad22024-07-11 08:01:13 +040080 envAppManagerAddr string,
gio23bdc1b2024-07-11 16:07:47 +040081 networks []string,
gio33059762024-07-05 13:19:07 +040082 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040083 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +040084 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +040085) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +040086 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
87 if err != nil {
88 return nil, err
89 }
gio81246f02024-07-10 12:02:15 +040090 sc := securecookie.New(
91 securecookie.GenerateRandomKey(64),
92 securecookie.GenerateRandomKey(32),
93 )
gio9d66f322024-07-06 13:45:10 +040094 s := &DodoAppServer{
95 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +040096 st,
gio0eaf2712024-04-14 13:08:46 +040097 port,
gioa60f0de2024-07-08 10:49:48 +040098 apiPort,
gio33059762024-07-05 13:19:07 +040099 self,
gio0eaf2712024-04-14 13:08:46 +0400100 sshKey,
gio33059762024-07-05 13:19:07 +0400101 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400102 client,
103 namespace,
giocb34ad22024-07-11 08:01:13 +0400104 envAppManagerAddr,
gio23bdc1b2024-07-11 16:07:47 +0400105 networks,
gio0eaf2712024-04-14 13:08:46 +0400106 env,
gio33059762024-07-05 13:19:07 +0400107 nsc,
giof8843412024-05-22 16:38:05 +0400108 jc,
gio266c04f2024-07-03 14:18:45 +0400109 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +0400110 map[string]string{},
gio81246f02024-07-10 12:02:15 +0400111 sc,
gio23bdc1b2024-07-11 16:07:47 +0400112 tmplts,
gio0eaf2712024-04-14 13:08:46 +0400113 }
gioa60f0de2024-07-08 10:49:48 +0400114 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400115 if err != nil {
116 return nil, err
117 }
118 r, err := config.Reader(namespacesFile)
119 if err == nil {
120 defer r.Close()
121 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
122 return nil, err
123 }
124 } else if !errors.Is(err, fs.ErrNotExist) {
125 return nil, err
126 }
127 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400128}
129
130func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400131 e := make(chan error)
132 go func() {
133 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400134 r.Use(s.mwAuth)
135 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
136 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
137 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
138 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
139 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gioa60f0de2024-07-08 10:49:48 +0400140 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
141 }()
142 go func() {
143 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400144 r.HandleFunc("/update", s.handleApiUpdate)
145 r.HandleFunc("/api/apps/{app-name}/workers", s.handleApiRegisterWorker).Methods(http.MethodPost)
146 r.HandleFunc("/api/apps", s.handleApiCreateApp).Methods(http.MethodPost)
147 r.HandleFunc("/api/add-admin-key", s.handleApiAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400148 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
149 }()
150 return <-e
151}
152
gio81246f02024-07-10 12:02:15 +0400153func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
154 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155 if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) {
156 next.ServeHTTP(w, r)
157 return
158 }
159 cookie, err := r.Cookie(sessionCookie)
160 if err != nil {
161 vars := mux.Vars(r)
162 appName, ok := vars["app-name"]
163 if !ok || appName == "" {
164 http.Error(w, "missing app-name", http.StatusBadRequest)
165 return
166 }
167 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
168 return
169 }
170 var user string
171 if err := s.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
172 http.Error(w, "unauthorized", http.StatusUnauthorized)
173 return
174 }
175 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
176 })
177}
178
179func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
180 http.SetCookie(w, &http.Cookie{
181 Name: sessionCookie,
182 Value: "",
183 Path: "/",
184 HttpOnly: true,
185 Secure: true,
186 })
187 http.Redirect(w, r, "/", http.StatusSeeOther)
188}
189
190func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
191 vars := mux.Vars(r)
192 appName, ok := vars["app-name"]
193 if !ok || appName == "" {
194 http.Error(w, "missing app-name", http.StatusBadRequest)
195 return
196 }
197 fmt.Fprint(w, `
198<!DOCTYPE html>
199<html lang='en'>
200 <head>
201 <title>dodo: app - login</title>
202 <meta charset='utf-8'>
203 </head>
204 <body>
205 <form action="" method="POST">
206 <input type="password" placeholder="Password" name="password" required />
207 <button type="submit">Login</button>
208 </form>
209 </body>
210</html>
211`)
212}
213
214func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
215 vars := mux.Vars(r)
216 appName, ok := vars["app-name"]
217 if !ok || appName == "" {
218 http.Error(w, "missing app-name", http.StatusBadRequest)
219 return
220 }
221 password := r.FormValue("password")
222 if password == "" {
223 http.Error(w, "missing password", http.StatusBadRequest)
224 return
225 }
226 user, err := s.st.GetAppOwner(appName)
227 if err != nil {
228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 return
230 }
231 hashed, err := s.st.GetUserPassword(user)
232 if err != nil {
233 http.Error(w, err.Error(), http.StatusInternalServerError)
234 return
235 }
236 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
237 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
238 return
239 }
240 if encoded, err := s.sc.Encode(sessionCookie, user); err == nil {
241 cookie := &http.Cookie{
242 Name: sessionCookie,
243 Value: encoded,
244 Path: "/",
245 Secure: true,
246 HttpOnly: true,
247 }
248 http.SetCookie(w, cookie)
249 }
250 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
251}
252
gio23bdc1b2024-07-11 16:07:47 +0400253type statusData struct {
254 Apps []string
255}
256
gioa60f0de2024-07-08 10:49:48 +0400257func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400258 user := r.Context().Value(userCtx)
259 if user == nil {
260 http.Error(w, "unauthorized", http.StatusUnauthorized)
261 return
262 }
263 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400264 if err != nil {
265 http.Error(w, err.Error(), http.StatusInternalServerError)
266 return
267 }
gio23bdc1b2024-07-11 16:07:47 +0400268 data := statusData{apps}
269 if err := s.tmplts.index.Execute(w, data); err != nil {
270 http.Error(w, err.Error(), http.StatusInternalServerError)
271 return
gioa60f0de2024-07-08 10:49:48 +0400272 }
273}
274
275func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
276 vars := mux.Vars(r)
277 appName, ok := vars["app-name"]
278 if !ok || appName == "" {
279 http.Error(w, "missing app-name", http.StatusBadRequest)
280 return
281 }
282 commits, err := s.st.GetCommitHistory(appName)
283 if err != nil {
284 http.Error(w, err.Error(), http.StatusInternalServerError)
285 return
286 }
287 for _, c := range commits {
288 fmt.Fprintf(w, "%s %s\n", c.Hash, c.Message)
289 }
gio0eaf2712024-04-14 13:08:46 +0400290}
291
gio81246f02024-07-10 12:02:15 +0400292type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400293 Ref string `json:"ref"`
294 Repository struct {
295 Name string `json:"name"`
296 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400297 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400298}
299
gio81246f02024-07-10 12:02:15 +0400300func (s *DodoAppServer) handleApiUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400301 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400302 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400303 var contents strings.Builder
304 io.Copy(&contents, r.Body)
305 c := contents.String()
306 fmt.Println(c)
307 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400308 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400309 return
310 }
gioa60f0de2024-07-08 10:49:48 +0400311 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400312 return
313 }
gioa60f0de2024-07-08 10:49:48 +0400314 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400315 go func() {
gio23bdc1b2024-07-11 16:07:47 +0400316 networks, err := s.getNetworks()
giocb34ad22024-07-11 08:01:13 +0400317 if err != nil {
318 return
319 }
320 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400321 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
322 fmt.Printf("Error: %s\n", err.Error())
323 return
324 }
325 }
326 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
327 fmt.Printf("Error: %s\n", err.Error())
328 }
329 for addr, _ := range s.workers[req.Repository.Name] {
330 go func() {
331 // TODO(gio): make port configurable
332 http.Get(fmt.Sprintf("http://%s/update", addr))
333 }()
gio0eaf2712024-04-14 13:08:46 +0400334 }
335 }()
gio0eaf2712024-04-14 13:08:46 +0400336}
337
gio81246f02024-07-10 12:02:15 +0400338type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400339 Address string `json:"address"`
340}
341
gio81246f02024-07-10 12:02:15 +0400342func (s *DodoAppServer) handleApiRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400343 vars := mux.Vars(r)
344 appName, ok := vars["app-name"]
345 if !ok || appName == "" {
346 http.Error(w, "missing app-name", http.StatusBadRequest)
347 return
348 }
gio81246f02024-07-10 12:02:15 +0400349 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400350 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
351 http.Error(w, err.Error(), http.StatusInternalServerError)
352 return
353 }
gioa60f0de2024-07-08 10:49:48 +0400354 if _, ok := s.workers[appName]; !ok {
355 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400356 }
gioa60f0de2024-07-08 10:49:48 +0400357 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400358}
359
gio81246f02024-07-10 12:02:15 +0400360type apiCreateAppReq struct {
gio33059762024-07-05 13:19:07 +0400361 AdminPublicKey string `json:"adminPublicKey"`
gio23bdc1b2024-07-11 16:07:47 +0400362 NetworkName string `json:"networkName"`
gio33059762024-07-05 13:19:07 +0400363}
364
gio81246f02024-07-10 12:02:15 +0400365type apiCreateAppResp struct {
366 AppName string `json:"appName"`
367 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400368}
369
gio81246f02024-07-10 12:02:15 +0400370func (s *DodoAppServer) handleApiCreateApp(w http.ResponseWriter, r *http.Request) {
371 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400372 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
373 http.Error(w, err.Error(), http.StatusBadRequest)
374 return
375 }
376 g := installer.NewFixedLengthRandomNameGenerator(3)
377 appName, err := g.Generate()
378 if err != nil {
379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
381 }
gio23bdc1b2024-07-11 16:07:47 +0400382 password, err := s.CreateApp(appName, req.AdminPublicKey, req.NetworkName)
gio81246f02024-07-10 12:02:15 +0400383 if err != nil {
gio33059762024-07-05 13:19:07 +0400384 http.Error(w, err.Error(), http.StatusInternalServerError)
385 return
386 }
gio81246f02024-07-10 12:02:15 +0400387 resp := apiCreateAppResp{
388 AppName: appName,
389 Password: password,
390 }
gio33059762024-07-05 13:19:07 +0400391 if err := json.NewEncoder(w).Encode(resp); err != nil {
392 http.Error(w, err.Error(), http.StatusInternalServerError)
393 return
394 }
395}
396
gio23bdc1b2024-07-11 16:07:47 +0400397func (s *DodoAppServer) CreateApp(appName, adminPublicKey, networkName string) (string, error) {
gio9d66f322024-07-06 13:45:10 +0400398 s.l.Lock()
399 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400400 fmt.Printf("Creating app: %s\n", appName)
401 if ok, err := s.client.RepoExists(appName); err != nil {
gio81246f02024-07-10 12:02:15 +0400402 return "", err
gio33059762024-07-05 13:19:07 +0400403 } else if ok {
gio81246f02024-07-10 12:02:15 +0400404 return "", nil
gio33059762024-07-05 13:19:07 +0400405 }
gio81246f02024-07-10 12:02:15 +0400406 user, err := s.client.FindUser(adminPublicKey)
407 if err != nil {
408 return "", err
409 }
gio23bdc1b2024-07-11 16:07:47 +0400410 if user == "" {
gio81246f02024-07-10 12:02:15 +0400411 user = appName
412 if err := s.client.AddUser(user, adminPublicKey); err != nil {
413 return "", err
414 }
415 }
416 password := generatePassword()
417 // TODO(gio): take admin password for initial application as input
418 if appName == "app" {
419 password = "app"
420 }
421 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
422 if err != nil {
423 return "", err
424 }
425 if err := s.st.CreateUser(user, hashed); err != nil {
426 if !errors.Is(err, ErrorAlreadyExists) {
427 return "", err
428 } else {
429 password = ""
430 }
431 }
432 if err := s.st.CreateApp(appName, user); err != nil {
433 return "", err
gioa60f0de2024-07-08 10:49:48 +0400434 }
gio33059762024-07-05 13:19:07 +0400435 if err := s.client.AddRepository(appName); err != nil {
gio81246f02024-07-10 12:02:15 +0400436 return "", err
gio33059762024-07-05 13:19:07 +0400437 }
438 appRepo, err := s.client.GetRepo(appName)
439 if err != nil {
gio81246f02024-07-10 12:02:15 +0400440 return "", err
gio33059762024-07-05 13:19:07 +0400441 }
gio23bdc1b2024-07-11 16:07:47 +0400442 if err := InitRepo(appRepo, networkName); err != nil {
gio81246f02024-07-10 12:02:15 +0400443 return "", err
gio33059762024-07-05 13:19:07 +0400444 }
445 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
446 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
447 if err != nil {
gio81246f02024-07-10 12:02:15 +0400448 return "", err
gio33059762024-07-05 13:19:07 +0400449 }
450 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
451 suffix, err := suffixGen.Generate()
452 if err != nil {
gio81246f02024-07-10 12:02:15 +0400453 return "", err
gio33059762024-07-05 13:19:07 +0400454 }
455 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
456 s.appNs[appName] = namespace
gio23bdc1b2024-07-11 16:07:47 +0400457 networks, err := s.getNetworks()
giocb34ad22024-07-11 08:01:13 +0400458 if err != nil {
459 return "", err
460 }
461 if err := s.updateDodoApp(appName, namespace, networks); err != nil {
gio81246f02024-07-10 12:02:15 +0400462 return "", err
gio33059762024-07-05 13:19:07 +0400463 }
gioa60f0de2024-07-08 10:49:48 +0400464 repo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400465 if err != nil {
gio81246f02024-07-10 12:02:15 +0400466 return "", err
gio33059762024-07-05 13:19:07 +0400467 }
468 hf := installer.NewGitHelmFetcher()
469 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
470 if err != nil {
gio81246f02024-07-10 12:02:15 +0400471 return "", err
gio33059762024-07-05 13:19:07 +0400472 }
gio9d66f322024-07-06 13:45:10 +0400473 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
474 w, err := fs.Writer(namespacesFile)
475 if err != nil {
476 return "", err
477 }
478 defer w.Close()
479 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
480 return "", err
481 }
482 if _, err := m.Install(
483 app,
484 appName,
485 "/"+appName,
486 namespace,
487 map[string]any{
488 "repoAddr": s.client.GetRepoAddress(appName),
489 "repoHost": strings.Split(s.client.Address(), ":")[0],
490 "gitRepoPublicKey": s.gitRepoPublicKey,
491 },
492 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400493 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400494 installer.WithNoPublish(),
495 installer.WithNoLock(),
496 ); err != nil {
497 return "", err
498 }
499 return fmt.Sprintf("Installed app: %s", appName), nil
500 }); err != nil {
gio81246f02024-07-10 12:02:15 +0400501 return "", err
gio33059762024-07-05 13:19:07 +0400502 }
503 cfg, err := m.FindInstance(appName)
504 if err != nil {
gio81246f02024-07-10 12:02:15 +0400505 return "", err
gio33059762024-07-05 13:19:07 +0400506 }
507 fluxKeys, ok := cfg.Input["fluxKeys"]
508 if !ok {
gio81246f02024-07-10 12:02:15 +0400509 return "", fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400510 }
511 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
512 if !ok {
gio81246f02024-07-10 12:02:15 +0400513 return "", fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400514 }
515 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio81246f02024-07-10 12:02:15 +0400516 return "", err
gio33059762024-07-05 13:19:07 +0400517 } else if ok {
518 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio81246f02024-07-10 12:02:15 +0400519 return "", err
gio33059762024-07-05 13:19:07 +0400520 }
521 } else {
522 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio81246f02024-07-10 12:02:15 +0400523 return "", err
gio33059762024-07-05 13:19:07 +0400524 }
525 }
526 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio81246f02024-07-10 12:02:15 +0400527 return "", err
gio33059762024-07-05 13:19:07 +0400528 }
529 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 +0400530 return "", err
gio33059762024-07-05 13:19:07 +0400531 }
gio81246f02024-07-10 12:02:15 +0400532 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
533 return "", err
gio33059762024-07-05 13:19:07 +0400534 }
gio81246f02024-07-10 12:02:15 +0400535 return password, nil
gio33059762024-07-05 13:19:07 +0400536}
537
gio81246f02024-07-10 12:02:15 +0400538type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400539 Public string `json:"public"`
540}
541
gio81246f02024-07-10 12:02:15 +0400542func (s *DodoAppServer) handleApiAddAdminKey(w http.ResponseWriter, r *http.Request) {
543 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400544 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
545 http.Error(w, err.Error(), http.StatusBadRequest)
546 return
547 }
548 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
551 }
552}
553
giocb34ad22024-07-11 08:01:13 +0400554func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400555 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400556 if err != nil {
557 return err
558 }
giof8843412024-05-22 16:38:05 +0400559 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400560 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400561 if err != nil {
562 return err
563 }
564 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400565 if err != nil {
566 return err
567 }
568 app, err := installer.NewDodoApp(appCfg)
569 if err != nil {
570 return err
571 }
giof8843412024-05-22 16:38:05 +0400572 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400573 if _, err := m.Install(
574 app,
575 "app",
576 "/.dodo/app",
577 namespace,
578 map[string]any{
gioa60f0de2024-07-08 10:49:48 +0400579 "repoAddr": repo.FullAddress(),
580 "managerAddr": fmt.Sprintf("http://%s", s.self),
581 "appId": name,
582 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400583 },
gio33059762024-07-05 13:19:07 +0400584 installer.WithConfig(&s.env),
giocb34ad22024-07-11 08:01:13 +0400585 installer.WithNetworks(networks),
giof71a0832024-06-27 14:45:45 +0400586 installer.WithLocalChartGenerator(lg),
587 installer.WithBranch("dodo"),
588 installer.WithForce(),
589 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400590 return err
591 }
592 return nil
593}
gio33059762024-07-05 13:19:07 +0400594
595const goMod = `module dodo.app
596
597go 1.18
598`
599
600const mainGo = `package main
601
602import (
603 "flag"
604 "fmt"
605 "log"
606 "net/http"
607)
608
609var port = flag.Int("port", 8080, "Port to listen on")
610
611func handler(w http.ResponseWriter, r *http.Request) {
612 fmt.Fprintln(w, "Hello from Dodo App!")
613}
614
615func main() {
616 flag.Parse()
617 http.HandleFunc("/", handler)
618 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
619}
620`
621
622const appCue = `app: {
623 type: "golang:1.22.0"
624 run: "main.go"
625 ingress: {
gio23bdc1b2024-07-11 16:07:47 +0400626 network: "%s"
gio33059762024-07-05 13:19:07 +0400627 subdomain: "testapp"
628 auth: enabled: false
629 }
630}
631`
632
gio23bdc1b2024-07-11 16:07:47 +0400633func InitRepo(repo soft.RepoIO, networkName string) error {
gio33059762024-07-05 13:19:07 +0400634 return repo.Do(func(fs soft.RepoFS) (string, error) {
635 {
636 w, err := fs.Writer("go.mod")
637 if err != nil {
638 return "", err
639 }
640 defer w.Close()
641 fmt.Fprint(w, goMod)
642 }
643 {
644 w, err := fs.Writer("main.go")
645 if err != nil {
646 return "", err
647 }
648 defer w.Close()
649 fmt.Fprintf(w, "%s", mainGo)
650 }
651 {
652 w, err := fs.Writer("app.cue")
653 if err != nil {
654 return "", err
655 }
656 defer w.Close()
gio23bdc1b2024-07-11 16:07:47 +0400657 fmt.Fprintf(w, appCue, networkName)
gio33059762024-07-05 13:19:07 +0400658 }
659 return "go web app template", nil
660 })
661}
gio81246f02024-07-10 12:02:15 +0400662
663func generatePassword() string {
664 return "foo"
665}
giocb34ad22024-07-11 08:01:13 +0400666
gio23bdc1b2024-07-11 16:07:47 +0400667func (s *DodoAppServer) getNetworks() ([]installer.Network, error) {
668 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400669 resp, err := http.Get(addr)
670 if err != nil {
671 return nil, err
672 }
gio23bdc1b2024-07-11 16:07:47 +0400673 networks := []installer.Network{}
674 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400675 return nil, err
676 }
gio23bdc1b2024-07-11 16:07:47 +0400677 if len(s.networks) == 0 {
678 return networks, nil
679 }
680 ret := []installer.Network{}
681 for _, n := range networks {
682 if slices.Contains(s.networks, n.Name) {
683 ret = append(ret, n)
684 }
685 }
giocb34ad22024-07-11 08:01:13 +0400686 return ret, nil
687}