blob: 87b003ad606ebac4462b92fc72d8586fa9449cb5 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
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"
gio81246f02024-07-10 12:02:15 +040010 "golang.org/x/crypto/bcrypt"
gio23bdc1b2024-07-11 16:07:47 +040011 "html/template"
gio0eaf2712024-04-14 13:08:46 +040012 "io"
gio9d66f322024-07-06 13:45:10 +040013 "io/fs"
gio0eaf2712024-04-14 13:08:46 +040014 "net/http"
gio23bdc1b2024-07-11 16:07:47 +040015 "slices"
gio0eaf2712024-04-14 13:08:46 +040016 "strings"
gio9d66f322024-07-06 13:45:10 +040017 "sync"
gio0eaf2712024-04-14 13:08:46 +040018
19 "github.com/giolekva/pcloud/core/installer"
20 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040021
22 "github.com/gorilla/mux"
gio81246f02024-07-10 12:02:15 +040023 "github.com/gorilla/securecookie"
gio0eaf2712024-04-14 13:08:46 +040024)
25
gio23bdc1b2024-07-11 16:07:47 +040026//go:embed dodo-app-tmpl/*
27var dodoAppTmplFS embed.FS
28
gio5e49bb62024-07-20 10:43:19 +040029//go:embed all:app-tmpl
30var appTmplsFS embed.FS
31
32//go:embed static
33var staticResources embed.FS
34
gio9d66f322024-07-06 13:45:10 +040035const (
gioa60f0de2024-07-08 10:49:48 +040036 ConfigRepoName = "config"
giod8ab4f52024-07-26 16:58:34 +040037 appConfigsFile = "/apps.json"
gio81246f02024-07-10 12:02:15 +040038 loginPath = "/login"
39 logoutPath = "/logout"
gio5e49bb62024-07-20 10:43:19 +040040 staticPath = "/static"
gio8fae3af2024-07-25 13:43:31 +040041 apiPublicData = "/api/public-data"
42 apiCreateApp = "/api/apps"
gio81246f02024-07-10 12:02:15 +040043 sessionCookie = "dodo-app-session"
44 userCtx = "user"
gio9d66f322024-07-06 13:45:10 +040045)
46
gio23bdc1b2024-07-11 16:07:47 +040047type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040048 index *template.Template
49 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040050}
51
52func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040053 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040054 if err != nil {
55 return dodoAppTmplts{}, err
56 }
gio5e49bb62024-07-20 10:43:19 +040057 parse := func(path string) (*template.Template, error) {
58 if b, err := base.Clone(); err != nil {
59 return nil, err
60 } else {
61 return b.ParseFS(fs, path)
62 }
63 }
64 index, err := parse("dodo-app-tmpl/index.html")
65 if err != nil {
66 return dodoAppTmplts{}, err
67 }
68 appStatus, err := parse("dodo-app-tmpl/app_status.html")
69 if err != nil {
70 return dodoAppTmplts{}, err
71 }
72 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040073}
74
gio0eaf2712024-04-14 13:08:46 +040075type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040076 l sync.Locker
77 st Store
gio11617ac2024-07-15 16:09:04 +040078 nf NetworkFilter
79 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040080 port int
81 apiPort int
82 self string
gio11617ac2024-07-15 16:09:04 +040083 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040084 sshKey string
85 gitRepoPublicKey string
86 client soft.Client
87 namespace string
88 envAppManagerAddr string
89 env installer.EnvConfig
90 nsc installer.NamespaceCreator
91 jc installer.JobCreator
92 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040093 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +040094 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040095 appTmpls AppTmplStore
giod8ab4f52024-07-26 16:58:34 +040096 allowNetworkReuse bool
97}
98
99type appConfig struct {
100 Namespace string `json:"namespace"`
101 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400102}
103
gio33059762024-07-05 13:19:07 +0400104// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400105func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400106 st Store,
gio11617ac2024-07-15 16:09:04 +0400107 nf NetworkFilter,
108 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400109 port int,
gioa60f0de2024-07-08 10:49:48 +0400110 apiPort int,
gio33059762024-07-05 13:19:07 +0400111 self string,
gio11617ac2024-07-15 16:09:04 +0400112 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400113 sshKey string,
gio33059762024-07-05 13:19:07 +0400114 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400115 client soft.Client,
116 namespace string,
giocb34ad22024-07-11 08:01:13 +0400117 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400118 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400119 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400120 env installer.EnvConfig,
giod8ab4f52024-07-26 16:58:34 +0400121 allowNetworkReuse bool,
gio9d66f322024-07-06 13:45:10 +0400122) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400123 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
124 if err != nil {
125 return nil, err
126 }
gio5e49bb62024-07-20 10:43:19 +0400127 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
128 if err != nil {
129 return nil, err
130 }
131 appTmpls, err := NewAppTmplStoreFS(apps)
132 if err != nil {
133 return nil, err
134 }
gio9d66f322024-07-06 13:45:10 +0400135 s := &DodoAppServer{
136 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400137 st,
gio11617ac2024-07-15 16:09:04 +0400138 nf,
139 ug,
gio0eaf2712024-04-14 13:08:46 +0400140 port,
gioa60f0de2024-07-08 10:49:48 +0400141 apiPort,
gio33059762024-07-05 13:19:07 +0400142 self,
gio11617ac2024-07-15 16:09:04 +0400143 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400144 sshKey,
gio33059762024-07-05 13:19:07 +0400145 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400146 client,
147 namespace,
giocb34ad22024-07-11 08:01:13 +0400148 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400149 env,
gio33059762024-07-05 13:19:07 +0400150 nsc,
giof8843412024-05-22 16:38:05 +0400151 jc,
gio266c04f2024-07-03 14:18:45 +0400152 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400153 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400154 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400155 appTmpls,
giod8ab4f52024-07-26 16:58:34 +0400156 allowNetworkReuse,
gio0eaf2712024-04-14 13:08:46 +0400157 }
gioa60f0de2024-07-08 10:49:48 +0400158 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400159 if err != nil {
160 return nil, err
161 }
giod8ab4f52024-07-26 16:58:34 +0400162 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400163 if err == nil {
164 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400165 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400166 return nil, err
167 }
168 } else if !errors.Is(err, fs.ErrNotExist) {
169 return nil, err
170 }
171 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400172}
173
174func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400175 e := make(chan error)
176 go func() {
177 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400178 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400179 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400180 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400181 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
182 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400183 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
184 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
185 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
186 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400187 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400188 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
189 }()
190 go func() {
191 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400192 r.HandleFunc("/update", s.handleAPIUpdate)
193 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
194 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400195 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
196 }()
197 return <-e
198}
199
gio11617ac2024-07-15 16:09:04 +0400200type UserGetter interface {
201 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400202 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400203}
204
205type externalUserGetter struct {
206 sc *securecookie.SecureCookie
207}
208
209func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400210 return &externalUserGetter{securecookie.New(
211 securecookie.GenerateRandomKey(64),
212 securecookie.GenerateRandomKey(32),
213 )}
gio11617ac2024-07-15 16:09:04 +0400214}
215
216func (ug *externalUserGetter) Get(r *http.Request) string {
217 cookie, err := r.Cookie(sessionCookie)
218 if err != nil {
219 return ""
220 }
221 var user string
222 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
223 return ""
224 }
225 return user
226}
227
gio8fae3af2024-07-25 13:43:31 +0400228func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
229 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
230 cookie := &http.Cookie{
231 Name: sessionCookie,
232 Value: encoded,
233 Path: "/",
234 Secure: true,
235 HttpOnly: true,
236 }
237 http.SetCookie(w, cookie)
238 return nil
239 } else {
240 return err
241 }
242}
243
gio11617ac2024-07-15 16:09:04 +0400244type internalUserGetter struct{}
245
246func NewInternalUserGetter() UserGetter {
247 return internalUserGetter{}
248}
249
250func (ug internalUserGetter) Get(r *http.Request) string {
251 return r.Header.Get("X-User")
252}
253
gio8fae3af2024-07-25 13:43:31 +0400254func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
255 return nil
256}
257
gio81246f02024-07-10 12:02:15 +0400258func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
259 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400260 if strings.HasSuffix(r.URL.Path, loginPath) ||
261 strings.HasPrefix(r.URL.Path, logoutPath) ||
262 strings.HasPrefix(r.URL.Path, staticPath) ||
263 strings.HasPrefix(r.URL.Path, apiPublicData) ||
264 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400265 next.ServeHTTP(w, r)
266 return
267 }
gio11617ac2024-07-15 16:09:04 +0400268 user := s.ug.Get(r)
269 if user == "" {
gio81246f02024-07-10 12:02:15 +0400270 vars := mux.Vars(r)
271 appName, ok := vars["app-name"]
272 if !ok || appName == "" {
273 http.Error(w, "missing app-name", http.StatusBadRequest)
274 return
275 }
276 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
277 return
278 }
gio81246f02024-07-10 12:02:15 +0400279 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
280 })
281}
282
283func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400284 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400285 http.SetCookie(w, &http.Cookie{
286 Name: sessionCookie,
287 Value: "",
288 Path: "/",
289 HttpOnly: true,
290 Secure: true,
291 })
292 http.Redirect(w, r, "/", http.StatusSeeOther)
293}
294
295func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
296 vars := mux.Vars(r)
297 appName, ok := vars["app-name"]
298 if !ok || appName == "" {
299 http.Error(w, "missing app-name", http.StatusBadRequest)
300 return
301 }
302 fmt.Fprint(w, `
303<!DOCTYPE html>
304<html lang='en'>
305 <head>
306 <title>dodo: app - login</title>
307 <meta charset='utf-8'>
308 </head>
309 <body>
310 <form action="" method="POST">
311 <input type="password" placeholder="Password" name="password" required />
312 <button type="submit">Login</button>
313 </form>
314 </body>
315</html>
316`)
317}
318
319func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
320 vars := mux.Vars(r)
321 appName, ok := vars["app-name"]
322 if !ok || appName == "" {
323 http.Error(w, "missing app-name", http.StatusBadRequest)
324 return
325 }
326 password := r.FormValue("password")
327 if password == "" {
328 http.Error(w, "missing password", http.StatusBadRequest)
329 return
330 }
331 user, err := s.st.GetAppOwner(appName)
332 if err != nil {
333 http.Error(w, err.Error(), http.StatusInternalServerError)
334 return
335 }
336 hashed, err := s.st.GetUserPassword(user)
337 if err != nil {
338 http.Error(w, err.Error(), http.StatusInternalServerError)
339 return
340 }
341 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
342 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
343 return
344 }
gio8fae3af2024-07-25 13:43:31 +0400345 if err := s.ug.Encode(w, user); err != nil {
346 http.Error(w, err.Error(), http.StatusInternalServerError)
347 return
gio81246f02024-07-10 12:02:15 +0400348 }
349 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
350}
351
gio23bdc1b2024-07-11 16:07:47 +0400352type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400353 Apps []string
354 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400355 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400356}
357
gioa60f0de2024-07-08 10:49:48 +0400358func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400359 user := r.Context().Value(userCtx)
360 if user == nil {
361 http.Error(w, "unauthorized", http.StatusUnauthorized)
362 return
363 }
364 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400365 if err != nil {
366 http.Error(w, err.Error(), http.StatusInternalServerError)
367 return
368 }
gio11617ac2024-07-15 16:09:04 +0400369 networks, err := s.getNetworks(user.(string))
370 if err != nil {
371 http.Error(w, err.Error(), http.StatusInternalServerError)
372 return
373 }
giob54db242024-07-30 18:49:33 +0400374 var types []string
375 for _, t := range s.appTmpls.Types() {
376 types = append(types, strings.Replace(t, "-", ":", 1))
377 }
gio5e49bb62024-07-20 10:43:19 +0400378 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400379 if err := s.tmplts.index.Execute(w, data); err != nil {
380 http.Error(w, err.Error(), http.StatusInternalServerError)
381 return
gioa60f0de2024-07-08 10:49:48 +0400382 }
383}
384
gio5e49bb62024-07-20 10:43:19 +0400385type appStatusData struct {
386 Name string
387 GitCloneCommand string
388 Commits []Commit
389}
390
gioa60f0de2024-07-08 10:49:48 +0400391func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
392 vars := mux.Vars(r)
393 appName, ok := vars["app-name"]
394 if !ok || appName == "" {
395 http.Error(w, "missing app-name", http.StatusBadRequest)
396 return
397 }
gio94904702024-07-26 16:58:34 +0400398 u := r.Context().Value(userCtx)
399 if u == nil {
400 http.Error(w, "unauthorized", http.StatusUnauthorized)
401 return
402 }
403 user, ok := u.(string)
404 if !ok {
405 http.Error(w, "could not get user", http.StatusInternalServerError)
406 return
407 }
408 owner, err := s.st.GetAppOwner(appName)
409 if err != nil {
410 http.Error(w, err.Error(), http.StatusInternalServerError)
411 return
412 }
413 if owner != user {
414 http.Error(w, "unauthorized", http.StatusUnauthorized)
415 return
416 }
gioa60f0de2024-07-08 10:49:48 +0400417 commits, err := s.st.GetCommitHistory(appName)
418 if err != nil {
419 http.Error(w, err.Error(), http.StatusInternalServerError)
420 return
421 }
gio5e49bb62024-07-20 10:43:19 +0400422 data := appStatusData{
423 Name: appName,
424 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
425 Commits: commits,
426 }
427 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
428 http.Error(w, err.Error(), http.StatusInternalServerError)
429 return
gioa60f0de2024-07-08 10:49:48 +0400430 }
gio0eaf2712024-04-14 13:08:46 +0400431}
432
gio81246f02024-07-10 12:02:15 +0400433type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400434 Ref string `json:"ref"`
435 Repository struct {
436 Name string `json:"name"`
437 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400438 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400439}
440
gio8fae3af2024-07-25 13:43:31 +0400441func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400442 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400443 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400444 var contents strings.Builder
445 io.Copy(&contents, r.Body)
446 c := contents.String()
447 fmt.Println(c)
448 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400449 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400450 return
451 }
gioa60f0de2024-07-08 10:49:48 +0400452 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400453 return
454 }
gioa60f0de2024-07-08 10:49:48 +0400455 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400456 go func() {
gio11617ac2024-07-15 16:09:04 +0400457 owner, err := s.st.GetAppOwner(req.Repository.Name)
458 if err != nil {
459 return
460 }
461 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400462 if err != nil {
463 return
464 }
gio94904702024-07-26 16:58:34 +0400465 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
466 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
467 if err != nil {
468 return
469 }
470 if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400471 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
472 fmt.Printf("Error: %s\n", err.Error())
473 return
474 }
475 }
476 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
477 fmt.Printf("Error: %s\n", err.Error())
478 }
479 for addr, _ := range s.workers[req.Repository.Name] {
480 go func() {
481 // TODO(gio): make port configurable
482 http.Get(fmt.Sprintf("http://%s/update", addr))
483 }()
gio0eaf2712024-04-14 13:08:46 +0400484 }
485 }()
gio0eaf2712024-04-14 13:08:46 +0400486}
487
gio81246f02024-07-10 12:02:15 +0400488type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400489 Address string `json:"address"`
490}
491
gio8fae3af2024-07-25 13:43:31 +0400492func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400493 vars := mux.Vars(r)
494 appName, ok := vars["app-name"]
495 if !ok || appName == "" {
496 http.Error(w, "missing app-name", http.StatusBadRequest)
497 return
498 }
gio81246f02024-07-10 12:02:15 +0400499 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400500 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
501 http.Error(w, err.Error(), http.StatusInternalServerError)
502 return
503 }
gioa60f0de2024-07-08 10:49:48 +0400504 if _, ok := s.workers[appName]; !ok {
505 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400506 }
gioa60f0de2024-07-08 10:49:48 +0400507 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400508}
509
gio11617ac2024-07-15 16:09:04 +0400510func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
511 u := r.Context().Value(userCtx)
512 if u == nil {
513 http.Error(w, "unauthorized", http.StatusUnauthorized)
514 return
515 }
516 user, ok := u.(string)
517 if !ok {
518 http.Error(w, "could not get user", http.StatusInternalServerError)
519 return
520 }
521 network := r.FormValue("network")
522 if network == "" {
523 http.Error(w, "missing network", http.StatusBadRequest)
524 return
525 }
gio5e49bb62024-07-20 10:43:19 +0400526 subdomain := r.FormValue("subdomain")
527 if subdomain == "" {
528 http.Error(w, "missing subdomain", http.StatusBadRequest)
529 return
530 }
531 appType := r.FormValue("type")
532 if appType == "" {
533 http.Error(w, "missing type", http.StatusBadRequest)
534 return
535 }
gio11617ac2024-07-15 16:09:04 +0400536 adminPublicKey := r.FormValue("admin-public-key")
gio5e49bb62024-07-20 10:43:19 +0400537 if adminPublicKey == "" {
gio11617ac2024-07-15 16:09:04 +0400538 http.Error(w, "missing admin public key", http.StatusBadRequest)
539 return
540 }
541 g := installer.NewFixedLengthRandomNameGenerator(3)
542 appName, err := g.Generate()
543 if err != nil {
544 http.Error(w, err.Error(), http.StatusInternalServerError)
545 return
546 }
547 if ok, err := s.client.UserExists(user); err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
550 } else if !ok {
551 if err := s.client.AddUser(user, adminPublicKey); err != nil {
552 http.Error(w, err.Error(), http.StatusInternalServerError)
553 return
554 }
555 }
556 if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
557 http.Error(w, err.Error(), http.StatusInternalServerError)
558 return
559 }
560 if err := s.st.CreateApp(appName, user); err != nil {
561 http.Error(w, err.Error(), http.StatusInternalServerError)
562 return
563 }
giod8ab4f52024-07-26 16:58:34 +0400564 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400565 http.Error(w, err.Error(), http.StatusInternalServerError)
566 return
567 }
568 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
569}
570
gio81246f02024-07-10 12:02:15 +0400571type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400572 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400573 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400574 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400575 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400576}
577
gio81246f02024-07-10 12:02:15 +0400578type apiCreateAppResp struct {
579 AppName string `json:"appName"`
580 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400581}
582
gio8fae3af2024-07-25 13:43:31 +0400583func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400584 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400585 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400586 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
587 http.Error(w, err.Error(), http.StatusBadRequest)
588 return
589 }
590 g := installer.NewFixedLengthRandomNameGenerator(3)
591 appName, err := g.Generate()
592 if err != nil {
593 http.Error(w, err.Error(), http.StatusInternalServerError)
594 return
595 }
gio11617ac2024-07-15 16:09:04 +0400596 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400597 if err != nil {
gio33059762024-07-05 13:19:07 +0400598 http.Error(w, err.Error(), http.StatusInternalServerError)
599 return
600 }
gio11617ac2024-07-15 16:09:04 +0400601 if user != "" {
602 http.Error(w, "public key already registered", http.StatusBadRequest)
603 return
604 }
605 user = appName
606 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
607 http.Error(w, err.Error(), http.StatusInternalServerError)
608 return
609 }
610 password := generatePassword()
611 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
612 if err != nil {
613 http.Error(w, err.Error(), http.StatusInternalServerError)
614 return
615 }
616 if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
617 http.Error(w, err.Error(), http.StatusInternalServerError)
618 return
619 }
620 if err := s.st.CreateApp(appName, user); err != nil {
621 http.Error(w, err.Error(), http.StatusInternalServerError)
622 return
623 }
giod8ab4f52024-07-26 16:58:34 +0400624 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400625 http.Error(w, err.Error(), http.StatusInternalServerError)
626 return
627 }
gio81246f02024-07-10 12:02:15 +0400628 resp := apiCreateAppResp{
629 AppName: appName,
630 Password: password,
631 }
gio33059762024-07-05 13:19:07 +0400632 if err := json.NewEncoder(w).Encode(resp); err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
636}
637
giod8ab4f52024-07-26 16:58:34 +0400638func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
639 if s.allowNetworkReuse {
640 return true
641 }
642 for _, cfg := range s.appConfigs {
643 if strings.ToLower(cfg.Network) == network {
644 return false
645 }
646 }
647 return true
648}
649
650func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400651 s.l.Lock()
652 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400653 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400654 network = strings.ToLower(network)
655 if !s.isNetworkUseAllowed(network) {
656 return fmt.Errorf("network already used: %s", network)
657 }
gio33059762024-07-05 13:19:07 +0400658 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400659 return err
gio33059762024-07-05 13:19:07 +0400660 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400661 return nil
gioa60f0de2024-07-08 10:49:48 +0400662 }
gio5e49bb62024-07-20 10:43:19 +0400663 networks, err := s.getNetworks(user)
664 if err != nil {
665 return err
666 }
giod8ab4f52024-07-26 16:58:34 +0400667 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400668 if !ok {
669 return fmt.Errorf("network not found: %s\n", network)
670 }
gio33059762024-07-05 13:19:07 +0400671 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400672 return err
gio33059762024-07-05 13:19:07 +0400673 }
674 appRepo, err := s.client.GetRepo(appName)
675 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400676 return err
gio33059762024-07-05 13:19:07 +0400677 }
gio5e49bb62024-07-20 10:43:19 +0400678 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400679 return err
gio33059762024-07-05 13:19:07 +0400680 }
681 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400682 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
683 if err != nil {
684 return err
685 }
686 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400687 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400688 return err
gio33059762024-07-05 13:19:07 +0400689 }
690 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
691 suffix, err := suffixGen.Generate()
692 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400693 return err
gio33059762024-07-05 13:19:07 +0400694 }
gio94904702024-07-26 16:58:34 +0400695 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400696 s.appConfigs[appName] = appConfig{namespace, network}
gio94904702024-07-26 16:58:34 +0400697 if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400698 return err
gio33059762024-07-05 13:19:07 +0400699 }
giod8ab4f52024-07-26 16:58:34 +0400700 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400701 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400702 return err
gio33059762024-07-05 13:19:07 +0400703 }
704 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400705 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400706 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400707 return err
gio33059762024-07-05 13:19:07 +0400708 }
giod8ab4f52024-07-26 16:58:34 +0400709 if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
710 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400711 if err != nil {
712 return "", err
713 }
714 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400715 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400716 return "", err
717 }
718 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400719 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400720 appName,
721 "/"+appName,
722 namespace,
723 map[string]any{
724 "repoAddr": s.client.GetRepoAddress(appName),
725 "repoHost": strings.Split(s.client.Address(), ":")[0],
726 "gitRepoPublicKey": s.gitRepoPublicKey,
727 },
728 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400729 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400730 installer.WithNoPublish(),
731 installer.WithNoLock(),
732 ); err != nil {
733 return "", err
734 }
735 return fmt.Sprintf("Installed app: %s", appName), nil
736 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400737 return err
gio33059762024-07-05 13:19:07 +0400738 }
739 cfg, err := m.FindInstance(appName)
740 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400741 return err
gio33059762024-07-05 13:19:07 +0400742 }
743 fluxKeys, ok := cfg.Input["fluxKeys"]
744 if !ok {
gio11617ac2024-07-15 16:09:04 +0400745 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400746 }
747 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
748 if !ok {
gio11617ac2024-07-15 16:09:04 +0400749 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400750 }
751 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400752 return err
gio33059762024-07-05 13:19:07 +0400753 } else if ok {
754 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400755 return err
gio33059762024-07-05 13:19:07 +0400756 }
757 } else {
758 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400759 return err
gio33059762024-07-05 13:19:07 +0400760 }
761 }
762 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400763 return err
gio33059762024-07-05 13:19:07 +0400764 }
765 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 +0400766 return err
gio33059762024-07-05 13:19:07 +0400767 }
gio81246f02024-07-10 12:02:15 +0400768 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400769 return err
gio33059762024-07-05 13:19:07 +0400770 }
gio11617ac2024-07-15 16:09:04 +0400771 return nil
gio33059762024-07-05 13:19:07 +0400772}
773
gio81246f02024-07-10 12:02:15 +0400774type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400775 Public string `json:"public"`
776}
777
gio8fae3af2024-07-25 13:43:31 +0400778func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400779 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400780 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
781 http.Error(w, err.Error(), http.StatusBadRequest)
782 return
783 }
784 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
785 http.Error(w, err.Error(), http.StatusInternalServerError)
786 return
787 }
788}
789
gio94904702024-07-26 16:58:34 +0400790type dodoAppRendered struct {
791 App struct {
792 Ingress struct {
793 Network string `json:"network"`
794 Subdomain string `json:"subdomain"`
795 } `json:"ingress"`
796 } `json:"app"`
797 Input struct {
798 AppId string `json:"appId"`
799 } `json:"input"`
800}
801
802func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400803 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400804 if err != nil {
805 return err
806 }
giof8843412024-05-22 16:38:05 +0400807 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400808 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400809 if err != nil {
810 return err
811 }
812 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400813 if err != nil {
814 return err
815 }
816 app, err := installer.NewDodoApp(appCfg)
817 if err != nil {
818 return err
819 }
giof8843412024-05-22 16:38:05 +0400820 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
gio94904702024-07-26 16:58:34 +0400821 return repo.Do(func(r soft.RepoFS) (string, error) {
822 res, err := m.Install(
823 app,
824 "app",
825 "/.dodo/app",
826 namespace,
827 map[string]any{
828 "repoAddr": repo.FullAddress(),
829 "managerAddr": fmt.Sprintf("http://%s", s.self),
830 "appId": name,
831 "sshPrivateKey": s.sshKey,
832 },
833 installer.WithNoPull(),
834 installer.WithNoPublish(),
835 installer.WithConfig(&s.env),
836 installer.WithNetworks(networks),
837 installer.WithLocalChartGenerator(lg),
838 installer.WithNoLock(),
839 )
840 if err != nil {
841 return "", err
842 }
843 var rendered dodoAppRendered
844 if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
845 return "", nil
846 }
847 if _, err := m.Install(
848 appStatus,
849 "status",
850 "/.dodo/status",
851 s.namespace,
852 map[string]any{
853 "appName": rendered.Input.AppId,
854 "network": rendered.App.Ingress.Network,
855 "appSubdomain": rendered.App.Ingress.Subdomain,
856 },
857 installer.WithNoPull(),
858 installer.WithNoPublish(),
859 installer.WithConfig(&s.env),
860 installer.WithNetworks(networks),
861 installer.WithLocalChartGenerator(lg),
862 installer.WithNoLock(),
863 ); err != nil {
864 return "", err
865 }
866 return "install app", nil
867 },
868 soft.WithCommitToBranch("dodo"),
869 soft.WithForce(),
870 )
gio0eaf2712024-04-14 13:08:46 +0400871}
gio33059762024-07-05 13:19:07 +0400872
gio5e49bb62024-07-20 10:43:19 +0400873func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
giob54db242024-07-30 18:49:33 +0400874 appType = strings.Replace(appType, ":", "-", 1)
gio5e49bb62024-07-20 10:43:19 +0400875 appTmpl, err := s.appTmpls.Find(appType)
876 if err != nil {
877 return err
gio33059762024-07-05 13:19:07 +0400878 }
gio33059762024-07-05 13:19:07 +0400879 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400880 if err := appTmpl.Render(network, subdomain, repo); err != nil {
881 return "", err
gio33059762024-07-05 13:19:07 +0400882 }
gio5e49bb62024-07-20 10:43:19 +0400883 return "init", nil
gio33059762024-07-05 13:19:07 +0400884 })
885}
gio81246f02024-07-10 12:02:15 +0400886
887func generatePassword() string {
888 return "foo"
889}
giocb34ad22024-07-11 08:01:13 +0400890
gio11617ac2024-07-15 16:09:04 +0400891func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400892 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400893 resp, err := http.Get(addr)
894 if err != nil {
895 return nil, err
896 }
gio23bdc1b2024-07-11 16:07:47 +0400897 networks := []installer.Network{}
898 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400899 return nil, err
900 }
gio11617ac2024-07-15 16:09:04 +0400901 return s.nf.Filter(user, networks)
902}
903
gio8fae3af2024-07-25 13:43:31 +0400904type publicNetworkData struct {
905 Name string `json:"name"`
906 Domain string `json:"domain"`
907}
908
909type publicData struct {
910 Networks []publicNetworkData `json:"networks"`
911 Types []string `json:"types"`
912}
913
914func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400915 w.Header().Set("Access-Control-Allow-Origin", "*")
916 s.l.Lock()
917 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +0400918 networks, err := s.getNetworks("")
919 if err != nil {
920 http.Error(w, err.Error(), http.StatusInternalServerError)
921 return
922 }
923 var ret publicData
924 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +0400925 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
926 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
927 }
gio8fae3af2024-07-25 13:43:31 +0400928 }
929 for _, t := range s.appTmpls.Types() {
giob54db242024-07-30 18:49:33 +0400930 ret.Types = append(ret.Types, strings.Replace(t, "-", ":", 1))
gio8fae3af2024-07-25 13:43:31 +0400931 }
gio8fae3af2024-07-25 13:43:31 +0400932 if err := json.NewEncoder(w).Encode(ret); err != nil {
933 http.Error(w, err.Error(), http.StatusInternalServerError)
934 return
935 }
936}
937
gio11617ac2024-07-15 16:09:04 +0400938func pickNetwork(networks []installer.Network, network string) []installer.Network {
939 for _, n := range networks {
940 if n.Name == network {
941 return []installer.Network{n}
942 }
943 }
944 return []installer.Network{}
945}
946
947type NetworkFilter interface {
948 Filter(user string, networks []installer.Network) ([]installer.Network, error)
949}
950
951type noNetworkFilter struct{}
952
953func NewNoNetworkFilter() NetworkFilter {
954 return noNetworkFilter{}
955}
956
gio8fae3af2024-07-25 13:43:31 +0400957func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400958 return networks, nil
959}
960
961type filterByOwner struct {
962 st Store
963}
964
965func NewNetworkFilterByOwner(st Store) NetworkFilter {
966 return &filterByOwner{st}
967}
968
969func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +0400970 if user == "" {
971 return networks, nil
972 }
gio11617ac2024-07-15 16:09:04 +0400973 network, err := f.st.GetUserNetwork(user)
974 if err != nil {
975 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400976 }
977 ret := []installer.Network{}
978 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400979 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400980 ret = append(ret, n)
981 }
982 }
giocb34ad22024-07-11 08:01:13 +0400983 return ret, nil
984}
gio11617ac2024-07-15 16:09:04 +0400985
986type allowListFilter struct {
987 allowed []string
988}
989
990func NewAllowListFilter(allowed []string) NetworkFilter {
991 return &allowListFilter{allowed}
992}
993
gio8fae3af2024-07-25 13:43:31 +0400994func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400995 ret := []installer.Network{}
996 for _, n := range networks {
997 if slices.Contains(f.allowed, n.Name) {
998 ret = append(ret, n)
999 }
1000 }
1001 return ret, nil
1002}
1003
1004type combinedNetworkFilter struct {
1005 filters []NetworkFilter
1006}
1007
1008func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1009 return &combinedNetworkFilter{filters}
1010}
1011
gio8fae3af2024-07-25 13:43:31 +04001012func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001013 ret := networks
1014 var err error
1015 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001016 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001017 if err != nil {
1018 return nil, err
1019 }
1020 }
1021 return ret, nil
1022}