blob: afcd62793cb2f85223d343722b155eac7c6f7418 [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
gio5e49bb62024-07-20 10:43:19 +040047var types = []string{"golang:1.22.0", "golang:1.20.0", "hugo:latest"}
48
gio23bdc1b2024-07-11 16:07:47 +040049type dodoAppTmplts struct {
gio5e49bb62024-07-20 10:43:19 +040050 index *template.Template
51 appStatus *template.Template
gio23bdc1b2024-07-11 16:07:47 +040052}
53
54func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
gio5e49bb62024-07-20 10:43:19 +040055 base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
gio23bdc1b2024-07-11 16:07:47 +040056 if err != nil {
57 return dodoAppTmplts{}, err
58 }
gio5e49bb62024-07-20 10:43:19 +040059 parse := func(path string) (*template.Template, error) {
60 if b, err := base.Clone(); err != nil {
61 return nil, err
62 } else {
63 return b.ParseFS(fs, path)
64 }
65 }
66 index, err := parse("dodo-app-tmpl/index.html")
67 if err != nil {
68 return dodoAppTmplts{}, err
69 }
70 appStatus, err := parse("dodo-app-tmpl/app_status.html")
71 if err != nil {
72 return dodoAppTmplts{}, err
73 }
74 return dodoAppTmplts{index, appStatus}, nil
gio23bdc1b2024-07-11 16:07:47 +040075}
76
gio0eaf2712024-04-14 13:08:46 +040077type DodoAppServer struct {
giocb34ad22024-07-11 08:01:13 +040078 l sync.Locker
79 st Store
gio11617ac2024-07-15 16:09:04 +040080 nf NetworkFilter
81 ug UserGetter
giocb34ad22024-07-11 08:01:13 +040082 port int
83 apiPort int
84 self string
gio11617ac2024-07-15 16:09:04 +040085 repoPublicAddr string
giocb34ad22024-07-11 08:01:13 +040086 sshKey string
87 gitRepoPublicKey string
88 client soft.Client
89 namespace string
90 envAppManagerAddr string
91 env installer.EnvConfig
92 nsc installer.NamespaceCreator
93 jc installer.JobCreator
94 workers map[string]map[string]struct{}
giod8ab4f52024-07-26 16:58:34 +040095 appConfigs map[string]appConfig
gio23bdc1b2024-07-11 16:07:47 +040096 tmplts dodoAppTmplts
gio5e49bb62024-07-20 10:43:19 +040097 appTmpls AppTmplStore
giod8ab4f52024-07-26 16:58:34 +040098 allowNetworkReuse bool
99}
100
101type appConfig struct {
102 Namespace string `json:"namespace"`
103 Network string `json:"network"`
gio0eaf2712024-04-14 13:08:46 +0400104}
105
gio33059762024-07-05 13:19:07 +0400106// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +0400107func NewDodoAppServer(
gioa60f0de2024-07-08 10:49:48 +0400108 st Store,
gio11617ac2024-07-15 16:09:04 +0400109 nf NetworkFilter,
110 ug UserGetter,
gio0eaf2712024-04-14 13:08:46 +0400111 port int,
gioa60f0de2024-07-08 10:49:48 +0400112 apiPort int,
gio33059762024-07-05 13:19:07 +0400113 self string,
gio11617ac2024-07-15 16:09:04 +0400114 repoPublicAddr string,
gio0eaf2712024-04-14 13:08:46 +0400115 sshKey string,
gio33059762024-07-05 13:19:07 +0400116 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +0400117 client soft.Client,
118 namespace string,
giocb34ad22024-07-11 08:01:13 +0400119 envAppManagerAddr string,
gio33059762024-07-05 13:19:07 +0400120 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +0400121 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +0400122 env installer.EnvConfig,
giod8ab4f52024-07-26 16:58:34 +0400123 allowNetworkReuse bool,
gio9d66f322024-07-06 13:45:10 +0400124) (*DodoAppServer, error) {
gio23bdc1b2024-07-11 16:07:47 +0400125 tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
126 if err != nil {
127 return nil, err
128 }
gio5e49bb62024-07-20 10:43:19 +0400129 apps, err := fs.Sub(appTmplsFS, "app-tmpl")
130 if err != nil {
131 return nil, err
132 }
133 appTmpls, err := NewAppTmplStoreFS(apps)
134 if err != nil {
135 return nil, err
136 }
gio9d66f322024-07-06 13:45:10 +0400137 s := &DodoAppServer{
138 &sync.Mutex{},
gioa60f0de2024-07-08 10:49:48 +0400139 st,
gio11617ac2024-07-15 16:09:04 +0400140 nf,
141 ug,
gio0eaf2712024-04-14 13:08:46 +0400142 port,
gioa60f0de2024-07-08 10:49:48 +0400143 apiPort,
gio33059762024-07-05 13:19:07 +0400144 self,
gio11617ac2024-07-15 16:09:04 +0400145 repoPublicAddr,
gio0eaf2712024-04-14 13:08:46 +0400146 sshKey,
gio33059762024-07-05 13:19:07 +0400147 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +0400148 client,
149 namespace,
giocb34ad22024-07-11 08:01:13 +0400150 envAppManagerAddr,
gio0eaf2712024-04-14 13:08:46 +0400151 env,
gio33059762024-07-05 13:19:07 +0400152 nsc,
giof8843412024-05-22 16:38:05 +0400153 jc,
gio266c04f2024-07-03 14:18:45 +0400154 map[string]map[string]struct{}{},
giod8ab4f52024-07-26 16:58:34 +0400155 map[string]appConfig{},
gio23bdc1b2024-07-11 16:07:47 +0400156 tmplts,
gio5e49bb62024-07-20 10:43:19 +0400157 appTmpls,
giod8ab4f52024-07-26 16:58:34 +0400158 allowNetworkReuse,
gio0eaf2712024-04-14 13:08:46 +0400159 }
gioa60f0de2024-07-08 10:49:48 +0400160 config, err := client.GetRepo(ConfigRepoName)
gio9d66f322024-07-06 13:45:10 +0400161 if err != nil {
162 return nil, err
163 }
giod8ab4f52024-07-26 16:58:34 +0400164 r, err := config.Reader(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400165 if err == nil {
166 defer r.Close()
giod8ab4f52024-07-26 16:58:34 +0400167 if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400168 return nil, err
169 }
170 } else if !errors.Is(err, fs.ErrNotExist) {
171 return nil, err
172 }
173 return s, nil
gio0eaf2712024-04-14 13:08:46 +0400174}
175
176func (s *DodoAppServer) Start() error {
gioa60f0de2024-07-08 10:49:48 +0400177 e := make(chan error)
178 go func() {
179 r := mux.NewRouter()
gio81246f02024-07-10 12:02:15 +0400180 r.Use(s.mwAuth)
gio5e49bb62024-07-20 10:43:19 +0400181 r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
gio81246f02024-07-10 12:02:15 +0400182 r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
gio8fae3af2024-07-25 13:43:31 +0400183 r.HandleFunc(apiPublicData, s.handleAPIPublicData)
184 r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
gio81246f02024-07-10 12:02:15 +0400185 r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
186 r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
187 r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
188 r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
gio11617ac2024-07-15 16:09:04 +0400189 r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400190 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
191 }()
192 go func() {
193 r := mux.NewRouter()
gio8fae3af2024-07-25 13:43:31 +0400194 r.HandleFunc("/update", s.handleAPIUpdate)
195 r.HandleFunc("/api/apps/{app-name}/workers", s.handleAPIRegisterWorker).Methods(http.MethodPost)
196 r.HandleFunc("/api/add-admin-key", s.handleAPIAddAdminKey).Methods(http.MethodPost)
gioa60f0de2024-07-08 10:49:48 +0400197 e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
198 }()
199 return <-e
200}
201
gio11617ac2024-07-15 16:09:04 +0400202type UserGetter interface {
203 Get(r *http.Request) string
gio8fae3af2024-07-25 13:43:31 +0400204 Encode(w http.ResponseWriter, user string) error
gio11617ac2024-07-15 16:09:04 +0400205}
206
207type externalUserGetter struct {
208 sc *securecookie.SecureCookie
209}
210
211func NewExternalUserGetter() UserGetter {
gio8fae3af2024-07-25 13:43:31 +0400212 return &externalUserGetter{securecookie.New(
213 securecookie.GenerateRandomKey(64),
214 securecookie.GenerateRandomKey(32),
215 )}
gio11617ac2024-07-15 16:09:04 +0400216}
217
218func (ug *externalUserGetter) Get(r *http.Request) string {
219 cookie, err := r.Cookie(sessionCookie)
220 if err != nil {
221 return ""
222 }
223 var user string
224 if err := ug.sc.Decode(sessionCookie, cookie.Value, &user); err != nil {
225 return ""
226 }
227 return user
228}
229
gio8fae3af2024-07-25 13:43:31 +0400230func (ug *externalUserGetter) Encode(w http.ResponseWriter, user string) error {
231 if encoded, err := ug.sc.Encode(sessionCookie, user); err == nil {
232 cookie := &http.Cookie{
233 Name: sessionCookie,
234 Value: encoded,
235 Path: "/",
236 Secure: true,
237 HttpOnly: true,
238 }
239 http.SetCookie(w, cookie)
240 return nil
241 } else {
242 return err
243 }
244}
245
gio11617ac2024-07-15 16:09:04 +0400246type internalUserGetter struct{}
247
248func NewInternalUserGetter() UserGetter {
249 return internalUserGetter{}
250}
251
252func (ug internalUserGetter) Get(r *http.Request) string {
253 return r.Header.Get("X-User")
254}
255
gio8fae3af2024-07-25 13:43:31 +0400256func (ug internalUserGetter) Encode(w http.ResponseWriter, user string) error {
257 return nil
258}
259
gio81246f02024-07-10 12:02:15 +0400260func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
261 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400262 if strings.HasSuffix(r.URL.Path, loginPath) ||
263 strings.HasPrefix(r.URL.Path, logoutPath) ||
264 strings.HasPrefix(r.URL.Path, staticPath) ||
265 strings.HasPrefix(r.URL.Path, apiPublicData) ||
266 strings.HasPrefix(r.URL.Path, apiCreateApp) {
gio81246f02024-07-10 12:02:15 +0400267 next.ServeHTTP(w, r)
268 return
269 }
gio11617ac2024-07-15 16:09:04 +0400270 user := s.ug.Get(r)
271 if user == "" {
gio81246f02024-07-10 12:02:15 +0400272 vars := mux.Vars(r)
273 appName, ok := vars["app-name"]
274 if !ok || appName == "" {
275 http.Error(w, "missing app-name", http.StatusBadRequest)
276 return
277 }
278 http.Redirect(w, r, fmt.Sprintf("/%s%s", appName, loginPath), http.StatusSeeOther)
279 return
280 }
gio81246f02024-07-10 12:02:15 +0400281 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userCtx, user)))
282 })
283}
284
285func (s *DodoAppServer) handleLogout(w http.ResponseWriter, r *http.Request) {
gio8fae3af2024-07-25 13:43:31 +0400286 // TODO(gio): move to UserGetter
gio81246f02024-07-10 12:02:15 +0400287 http.SetCookie(w, &http.Cookie{
288 Name: sessionCookie,
289 Value: "",
290 Path: "/",
291 HttpOnly: true,
292 Secure: true,
293 })
294 http.Redirect(w, r, "/", http.StatusSeeOther)
295}
296
297func (s *DodoAppServer) handleLoginForm(w http.ResponseWriter, r *http.Request) {
298 vars := mux.Vars(r)
299 appName, ok := vars["app-name"]
300 if !ok || appName == "" {
301 http.Error(w, "missing app-name", http.StatusBadRequest)
302 return
303 }
304 fmt.Fprint(w, `
305<!DOCTYPE html>
306<html lang='en'>
307 <head>
308 <title>dodo: app - login</title>
309 <meta charset='utf-8'>
310 </head>
311 <body>
312 <form action="" method="POST">
313 <input type="password" placeholder="Password" name="password" required />
314 <button type="submit">Login</button>
315 </form>
316 </body>
317</html>
318`)
319}
320
321func (s *DodoAppServer) handleLogin(w http.ResponseWriter, r *http.Request) {
322 vars := mux.Vars(r)
323 appName, ok := vars["app-name"]
324 if !ok || appName == "" {
325 http.Error(w, "missing app-name", http.StatusBadRequest)
326 return
327 }
328 password := r.FormValue("password")
329 if password == "" {
330 http.Error(w, "missing password", http.StatusBadRequest)
331 return
332 }
333 user, err := s.st.GetAppOwner(appName)
334 if err != nil {
335 http.Error(w, err.Error(), http.StatusInternalServerError)
336 return
337 }
338 hashed, err := s.st.GetUserPassword(user)
339 if err != nil {
340 http.Error(w, err.Error(), http.StatusInternalServerError)
341 return
342 }
343 if err := bcrypt.CompareHashAndPassword(hashed, []byte(password)); err != nil {
344 http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
345 return
346 }
gio8fae3af2024-07-25 13:43:31 +0400347 if err := s.ug.Encode(w, user); err != nil {
348 http.Error(w, err.Error(), http.StatusInternalServerError)
349 return
gio81246f02024-07-10 12:02:15 +0400350 }
351 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
352}
353
gio23bdc1b2024-07-11 16:07:47 +0400354type statusData struct {
gio11617ac2024-07-15 16:09:04 +0400355 Apps []string
356 Networks []installer.Network
gio5e49bb62024-07-20 10:43:19 +0400357 Types []string
gio23bdc1b2024-07-11 16:07:47 +0400358}
359
gioa60f0de2024-07-08 10:49:48 +0400360func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400361 user := r.Context().Value(userCtx)
362 if user == nil {
363 http.Error(w, "unauthorized", http.StatusUnauthorized)
364 return
365 }
366 apps, err := s.st.GetUserApps(user.(string))
gioa60f0de2024-07-08 10:49:48 +0400367 if err != nil {
368 http.Error(w, err.Error(), http.StatusInternalServerError)
369 return
370 }
gio11617ac2024-07-15 16:09:04 +0400371 networks, err := s.getNetworks(user.(string))
372 if err != nil {
373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 return
375 }
gio5e49bb62024-07-20 10:43:19 +0400376 data := statusData{apps, networks, types}
gio23bdc1b2024-07-11 16:07:47 +0400377 if err := s.tmplts.index.Execute(w, data); err != nil {
378 http.Error(w, err.Error(), http.StatusInternalServerError)
379 return
gioa60f0de2024-07-08 10:49:48 +0400380 }
381}
382
gio5e49bb62024-07-20 10:43:19 +0400383type appStatusData struct {
384 Name string
385 GitCloneCommand string
386 Commits []Commit
387}
388
gioa60f0de2024-07-08 10:49:48 +0400389func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
390 vars := mux.Vars(r)
391 appName, ok := vars["app-name"]
392 if !ok || appName == "" {
393 http.Error(w, "missing app-name", http.StatusBadRequest)
394 return
395 }
gio94904702024-07-26 16:58:34 +0400396 u := r.Context().Value(userCtx)
397 if u == nil {
398 http.Error(w, "unauthorized", http.StatusUnauthorized)
399 return
400 }
401 user, ok := u.(string)
402 if !ok {
403 http.Error(w, "could not get user", http.StatusInternalServerError)
404 return
405 }
406 owner, err := s.st.GetAppOwner(appName)
407 if err != nil {
408 http.Error(w, err.Error(), http.StatusInternalServerError)
409 return
410 }
411 if owner != user {
412 http.Error(w, "unauthorized", http.StatusUnauthorized)
413 return
414 }
gioa60f0de2024-07-08 10:49:48 +0400415 commits, err := s.st.GetCommitHistory(appName)
416 if err != nil {
417 http.Error(w, err.Error(), http.StatusInternalServerError)
418 return
419 }
gio5e49bb62024-07-20 10:43:19 +0400420 data := appStatusData{
421 Name: appName,
422 GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
423 Commits: commits,
424 }
425 if err := s.tmplts.appStatus.Execute(w, data); err != nil {
426 http.Error(w, err.Error(), http.StatusInternalServerError)
427 return
gioa60f0de2024-07-08 10:49:48 +0400428 }
gio0eaf2712024-04-14 13:08:46 +0400429}
430
gio81246f02024-07-10 12:02:15 +0400431type apiUpdateReq struct {
gio266c04f2024-07-03 14:18:45 +0400432 Ref string `json:"ref"`
433 Repository struct {
434 Name string `json:"name"`
435 } `json:"repository"`
gioa60f0de2024-07-08 10:49:48 +0400436 After string `json:"after"`
gio0eaf2712024-04-14 13:08:46 +0400437}
438
gio8fae3af2024-07-25 13:43:31 +0400439func (s *DodoAppServer) handleAPIUpdate(w http.ResponseWriter, r *http.Request) {
gio0eaf2712024-04-14 13:08:46 +0400440 fmt.Println("update")
gio81246f02024-07-10 12:02:15 +0400441 var req apiUpdateReq
gio0eaf2712024-04-14 13:08:46 +0400442 var contents strings.Builder
443 io.Copy(&contents, r.Body)
444 c := contents.String()
445 fmt.Println(c)
446 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
gio23bdc1b2024-07-11 16:07:47 +0400447 http.Error(w, err.Error(), http.StatusBadRequest)
gio0eaf2712024-04-14 13:08:46 +0400448 return
449 }
gioa60f0de2024-07-08 10:49:48 +0400450 if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
gio0eaf2712024-04-14 13:08:46 +0400451 return
452 }
gioa60f0de2024-07-08 10:49:48 +0400453 // TODO(gio): Create commit record on app init as well
gio0eaf2712024-04-14 13:08:46 +0400454 go func() {
gio11617ac2024-07-15 16:09:04 +0400455 owner, err := s.st.GetAppOwner(req.Repository.Name)
456 if err != nil {
457 return
458 }
459 networks, err := s.getNetworks(owner)
giocb34ad22024-07-11 08:01:13 +0400460 if err != nil {
461 return
462 }
gio94904702024-07-26 16:58:34 +0400463 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
464 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
465 if err != nil {
466 return
467 }
468 if err := s.updateDodoApp(instanceAppStatus, req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
gioa60f0de2024-07-08 10:49:48 +0400469 if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
470 fmt.Printf("Error: %s\n", err.Error())
471 return
472 }
473 }
474 if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
475 fmt.Printf("Error: %s\n", err.Error())
476 }
477 for addr, _ := range s.workers[req.Repository.Name] {
478 go func() {
479 // TODO(gio): make port configurable
480 http.Get(fmt.Sprintf("http://%s/update", addr))
481 }()
gio0eaf2712024-04-14 13:08:46 +0400482 }
483 }()
gio0eaf2712024-04-14 13:08:46 +0400484}
485
gio81246f02024-07-10 12:02:15 +0400486type apiRegisterWorkerReq struct {
gio0eaf2712024-04-14 13:08:46 +0400487 Address string `json:"address"`
488}
489
gio8fae3af2024-07-25 13:43:31 +0400490func (s *DodoAppServer) handleAPIRegisterWorker(w http.ResponseWriter, r *http.Request) {
gioa60f0de2024-07-08 10:49:48 +0400491 vars := mux.Vars(r)
492 appName, ok := vars["app-name"]
493 if !ok || appName == "" {
494 http.Error(w, "missing app-name", http.StatusBadRequest)
495 return
496 }
gio81246f02024-07-10 12:02:15 +0400497 var req apiRegisterWorkerReq
gio0eaf2712024-04-14 13:08:46 +0400498 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
499 http.Error(w, err.Error(), http.StatusInternalServerError)
500 return
501 }
gioa60f0de2024-07-08 10:49:48 +0400502 if _, ok := s.workers[appName]; !ok {
503 s.workers[appName] = map[string]struct{}{}
gio266c04f2024-07-03 14:18:45 +0400504 }
gioa60f0de2024-07-08 10:49:48 +0400505 s.workers[appName][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400506}
507
gio11617ac2024-07-15 16:09:04 +0400508func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
509 u := r.Context().Value(userCtx)
510 if u == nil {
511 http.Error(w, "unauthorized", http.StatusUnauthorized)
512 return
513 }
514 user, ok := u.(string)
515 if !ok {
516 http.Error(w, "could not get user", http.StatusInternalServerError)
517 return
518 }
519 network := r.FormValue("network")
520 if network == "" {
521 http.Error(w, "missing network", http.StatusBadRequest)
522 return
523 }
gio5e49bb62024-07-20 10:43:19 +0400524 subdomain := r.FormValue("subdomain")
525 if subdomain == "" {
526 http.Error(w, "missing subdomain", http.StatusBadRequest)
527 return
528 }
529 appType := r.FormValue("type")
530 if appType == "" {
531 http.Error(w, "missing type", http.StatusBadRequest)
532 return
533 }
gio11617ac2024-07-15 16:09:04 +0400534 adminPublicKey := r.FormValue("admin-public-key")
gio5e49bb62024-07-20 10:43:19 +0400535 if adminPublicKey == "" {
gio11617ac2024-07-15 16:09:04 +0400536 http.Error(w, "missing admin public key", http.StatusBadRequest)
537 return
538 }
539 g := installer.NewFixedLengthRandomNameGenerator(3)
540 appName, err := g.Generate()
541 if err != nil {
542 http.Error(w, err.Error(), http.StatusInternalServerError)
543 return
544 }
545 if ok, err := s.client.UserExists(user); err != nil {
546 http.Error(w, err.Error(), http.StatusInternalServerError)
547 return
548 } else if !ok {
549 if err := s.client.AddUser(user, adminPublicKey); err != nil {
550 http.Error(w, err.Error(), http.StatusInternalServerError)
551 return
552 }
553 }
554 if err := s.st.CreateUser(user, nil, adminPublicKey, network); err != nil && !errors.Is(err, ErrorAlreadyExists) {
555 http.Error(w, err.Error(), http.StatusInternalServerError)
556 return
557 }
558 if err := s.st.CreateApp(appName, user); err != nil {
559 http.Error(w, err.Error(), http.StatusInternalServerError)
560 return
561 }
giod8ab4f52024-07-26 16:58:34 +0400562 if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400563 http.Error(w, err.Error(), http.StatusInternalServerError)
564 return
565 }
566 http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
567}
568
gio81246f02024-07-10 12:02:15 +0400569type apiCreateAppReq struct {
gio5e49bb62024-07-20 10:43:19 +0400570 AppType string `json:"type"`
gio33059762024-07-05 13:19:07 +0400571 AdminPublicKey string `json:"adminPublicKey"`
gio11617ac2024-07-15 16:09:04 +0400572 Network string `json:"network"`
gio5e49bb62024-07-20 10:43:19 +0400573 Subdomain string `json:"subdomain"`
gio33059762024-07-05 13:19:07 +0400574}
575
gio81246f02024-07-10 12:02:15 +0400576type apiCreateAppResp struct {
577 AppName string `json:"appName"`
578 Password string `json:"password"`
gio33059762024-07-05 13:19:07 +0400579}
580
gio8fae3af2024-07-25 13:43:31 +0400581func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400582 w.Header().Set("Access-Control-Allow-Origin", "*")
gio81246f02024-07-10 12:02:15 +0400583 var req apiCreateAppReq
gio33059762024-07-05 13:19:07 +0400584 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
585 http.Error(w, err.Error(), http.StatusBadRequest)
586 return
587 }
588 g := installer.NewFixedLengthRandomNameGenerator(3)
589 appName, err := g.Generate()
590 if err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
gio11617ac2024-07-15 16:09:04 +0400594 user, err := s.client.FindUser(req.AdminPublicKey)
gio81246f02024-07-10 12:02:15 +0400595 if err != nil {
gio33059762024-07-05 13:19:07 +0400596 http.Error(w, err.Error(), http.StatusInternalServerError)
597 return
598 }
gio11617ac2024-07-15 16:09:04 +0400599 if user != "" {
600 http.Error(w, "public key already registered", http.StatusBadRequest)
601 return
602 }
603 user = appName
604 if err := s.client.AddUser(user, req.AdminPublicKey); err != nil {
605 http.Error(w, err.Error(), http.StatusInternalServerError)
606 return
607 }
608 password := generatePassword()
609 hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
610 if err != nil {
611 http.Error(w, err.Error(), http.StatusInternalServerError)
612 return
613 }
614 if err := s.st.CreateUser(user, hashed, req.AdminPublicKey, req.Network); err != nil {
615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
617 }
618 if err := s.st.CreateApp(appName, user); err != nil {
619 http.Error(w, err.Error(), http.StatusInternalServerError)
620 return
621 }
giod8ab4f52024-07-26 16:58:34 +0400622 if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400623 http.Error(w, err.Error(), http.StatusInternalServerError)
624 return
625 }
gio81246f02024-07-10 12:02:15 +0400626 resp := apiCreateAppResp{
627 AppName: appName,
628 Password: password,
629 }
gio33059762024-07-05 13:19:07 +0400630 if err := json.NewEncoder(w).Encode(resp); err != nil {
631 http.Error(w, err.Error(), http.StatusInternalServerError)
632 return
633 }
634}
635
giod8ab4f52024-07-26 16:58:34 +0400636func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
637 if s.allowNetworkReuse {
638 return true
639 }
640 for _, cfg := range s.appConfigs {
641 if strings.ToLower(cfg.Network) == network {
642 return false
643 }
644 }
645 return true
646}
647
648func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
gio9d66f322024-07-06 13:45:10 +0400649 s.l.Lock()
650 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400651 fmt.Printf("Creating app: %s\n", appName)
giod8ab4f52024-07-26 16:58:34 +0400652 network = strings.ToLower(network)
653 if !s.isNetworkUseAllowed(network) {
654 return fmt.Errorf("network already used: %s", network)
655 }
gio33059762024-07-05 13:19:07 +0400656 if ok, err := s.client.RepoExists(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400657 return err
gio33059762024-07-05 13:19:07 +0400658 } else if ok {
gio11617ac2024-07-15 16:09:04 +0400659 return nil
gioa60f0de2024-07-08 10:49:48 +0400660 }
gio5e49bb62024-07-20 10:43:19 +0400661 networks, err := s.getNetworks(user)
662 if err != nil {
663 return err
664 }
giod8ab4f52024-07-26 16:58:34 +0400665 n, ok := installer.NetworkMap(networks)[network]
gio5e49bb62024-07-20 10:43:19 +0400666 if !ok {
667 return fmt.Errorf("network not found: %s\n", network)
668 }
gio33059762024-07-05 13:19:07 +0400669 if err := s.client.AddRepository(appName); err != nil {
gio11617ac2024-07-15 16:09:04 +0400670 return err
gio33059762024-07-05 13:19:07 +0400671 }
672 appRepo, err := s.client.GetRepo(appName)
673 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400674 return err
gio33059762024-07-05 13:19:07 +0400675 }
gio5e49bb62024-07-20 10:43:19 +0400676 if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
gio11617ac2024-07-15 16:09:04 +0400677 return err
gio33059762024-07-05 13:19:07 +0400678 }
679 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
gio94904702024-07-26 16:58:34 +0400680 instanceApp, err := installer.FindEnvApp(apps, "dodo-app-instance")
681 if err != nil {
682 return err
683 }
684 instanceAppStatus, err := installer.FindEnvApp(apps, "dodo-app-instance-status")
gio33059762024-07-05 13:19:07 +0400685 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400686 return err
gio33059762024-07-05 13:19:07 +0400687 }
688 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
689 suffix, err := suffixGen.Generate()
690 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400691 return err
gio33059762024-07-05 13:19:07 +0400692 }
gio94904702024-07-26 16:58:34 +0400693 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, instanceApp.Namespace(), suffix)
giod8ab4f52024-07-26 16:58:34 +0400694 s.appConfigs[appName] = appConfig{namespace, network}
gio94904702024-07-26 16:58:34 +0400695 if err := s.updateDodoApp(instanceAppStatus, appName, namespace, networks); err != nil {
gio11617ac2024-07-15 16:09:04 +0400696 return err
gio33059762024-07-05 13:19:07 +0400697 }
giod8ab4f52024-07-26 16:58:34 +0400698 configRepo, err := s.client.GetRepo(ConfigRepoName)
gio33059762024-07-05 13:19:07 +0400699 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400700 return err
gio33059762024-07-05 13:19:07 +0400701 }
702 hf := installer.NewGitHelmFetcher()
giod8ab4f52024-07-26 16:58:34 +0400703 m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
gio33059762024-07-05 13:19:07 +0400704 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400705 return err
gio33059762024-07-05 13:19:07 +0400706 }
giod8ab4f52024-07-26 16:58:34 +0400707 if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
708 w, err := fs.Writer(appConfigsFile)
gio9d66f322024-07-06 13:45:10 +0400709 if err != nil {
710 return "", err
711 }
712 defer w.Close()
giod8ab4f52024-07-26 16:58:34 +0400713 if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
gio9d66f322024-07-06 13:45:10 +0400714 return "", err
715 }
716 if _, err := m.Install(
gio94904702024-07-26 16:58:34 +0400717 instanceApp,
gio9d66f322024-07-06 13:45:10 +0400718 appName,
719 "/"+appName,
720 namespace,
721 map[string]any{
722 "repoAddr": s.client.GetRepoAddress(appName),
723 "repoHost": strings.Split(s.client.Address(), ":")[0],
724 "gitRepoPublicKey": s.gitRepoPublicKey,
725 },
726 installer.WithConfig(&s.env),
gio23bdc1b2024-07-11 16:07:47 +0400727 installer.WithNoNetworks(),
gio9d66f322024-07-06 13:45:10 +0400728 installer.WithNoPublish(),
729 installer.WithNoLock(),
730 ); err != nil {
731 return "", err
732 }
733 return fmt.Sprintf("Installed app: %s", appName), nil
734 }); err != nil {
gio11617ac2024-07-15 16:09:04 +0400735 return err
gio33059762024-07-05 13:19:07 +0400736 }
737 cfg, err := m.FindInstance(appName)
738 if err != nil {
gio11617ac2024-07-15 16:09:04 +0400739 return err
gio33059762024-07-05 13:19:07 +0400740 }
741 fluxKeys, ok := cfg.Input["fluxKeys"]
742 if !ok {
gio11617ac2024-07-15 16:09:04 +0400743 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400744 }
745 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
746 if !ok {
gio11617ac2024-07-15 16:09:04 +0400747 return fmt.Errorf("Fluxcd keys not found")
gio33059762024-07-05 13:19:07 +0400748 }
749 if ok, err := s.client.UserExists("fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400750 return err
gio33059762024-07-05 13:19:07 +0400751 } else if ok {
752 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400753 return err
gio33059762024-07-05 13:19:07 +0400754 }
755 } else {
756 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
gio11617ac2024-07-15 16:09:04 +0400757 return err
gio33059762024-07-05 13:19:07 +0400758 }
759 }
760 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
gio11617ac2024-07-15 16:09:04 +0400761 return err
gio33059762024-07-05 13:19:07 +0400762 }
763 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 +0400764 return err
gio33059762024-07-05 13:19:07 +0400765 }
gio81246f02024-07-10 12:02:15 +0400766 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
gio11617ac2024-07-15 16:09:04 +0400767 return err
gio33059762024-07-05 13:19:07 +0400768 }
gio11617ac2024-07-15 16:09:04 +0400769 return nil
gio33059762024-07-05 13:19:07 +0400770}
771
gio81246f02024-07-10 12:02:15 +0400772type apiAddAdminKeyReq struct {
gio70be3e52024-06-26 18:27:19 +0400773 Public string `json:"public"`
774}
775
gio8fae3af2024-07-25 13:43:31 +0400776func (s *DodoAppServer) handleAPIAddAdminKey(w http.ResponseWriter, r *http.Request) {
gio81246f02024-07-10 12:02:15 +0400777 var req apiAddAdminKeyReq
gio70be3e52024-06-26 18:27:19 +0400778 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
779 http.Error(w, err.Error(), http.StatusBadRequest)
780 return
781 }
782 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
783 http.Error(w, err.Error(), http.StatusInternalServerError)
784 return
785 }
786}
787
gio94904702024-07-26 16:58:34 +0400788type dodoAppRendered struct {
789 App struct {
790 Ingress struct {
791 Network string `json:"network"`
792 Subdomain string `json:"subdomain"`
793 } `json:"ingress"`
794 } `json:"app"`
795 Input struct {
796 AppId string `json:"appId"`
797 } `json:"input"`
798}
799
800func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
gio33059762024-07-05 13:19:07 +0400801 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400802 if err != nil {
803 return err
804 }
giof8843412024-05-22 16:38:05 +0400805 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400806 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400807 if err != nil {
808 return err
809 }
810 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400811 if err != nil {
812 return err
813 }
814 app, err := installer.NewDodoApp(appCfg)
815 if err != nil {
816 return err
817 }
giof8843412024-05-22 16:38:05 +0400818 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
gio94904702024-07-26 16:58:34 +0400819 return repo.Do(func(r soft.RepoFS) (string, error) {
820 res, err := m.Install(
821 app,
822 "app",
823 "/.dodo/app",
824 namespace,
825 map[string]any{
826 "repoAddr": repo.FullAddress(),
827 "managerAddr": fmt.Sprintf("http://%s", s.self),
828 "appId": name,
829 "sshPrivateKey": s.sshKey,
830 },
831 installer.WithNoPull(),
832 installer.WithNoPublish(),
833 installer.WithConfig(&s.env),
834 installer.WithNetworks(networks),
835 installer.WithLocalChartGenerator(lg),
836 installer.WithNoLock(),
837 )
838 if err != nil {
839 return "", err
840 }
841 var rendered dodoAppRendered
842 if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
843 return "", nil
844 }
845 if _, err := m.Install(
846 appStatus,
847 "status",
848 "/.dodo/status",
849 s.namespace,
850 map[string]any{
851 "appName": rendered.Input.AppId,
852 "network": rendered.App.Ingress.Network,
853 "appSubdomain": rendered.App.Ingress.Subdomain,
854 },
855 installer.WithNoPull(),
856 installer.WithNoPublish(),
857 installer.WithConfig(&s.env),
858 installer.WithNetworks(networks),
859 installer.WithLocalChartGenerator(lg),
860 installer.WithNoLock(),
861 ); err != nil {
862 return "", err
863 }
864 return "install app", nil
865 },
866 soft.WithCommitToBranch("dodo"),
867 soft.WithForce(),
868 )
gio0eaf2712024-04-14 13:08:46 +0400869}
gio33059762024-07-05 13:19:07 +0400870
gio5e49bb62024-07-20 10:43:19 +0400871func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
872 appType = strings.ReplaceAll(appType, ":", "-")
873 appTmpl, err := s.appTmpls.Find(appType)
874 if err != nil {
875 return err
gio33059762024-07-05 13:19:07 +0400876 }
gio33059762024-07-05 13:19:07 +0400877 return repo.Do(func(fs soft.RepoFS) (string, error) {
gio5e49bb62024-07-20 10:43:19 +0400878 if err := appTmpl.Render(network, subdomain, repo); err != nil {
879 return "", err
gio33059762024-07-05 13:19:07 +0400880 }
gio5e49bb62024-07-20 10:43:19 +0400881 return "init", nil
gio33059762024-07-05 13:19:07 +0400882 })
883}
gio81246f02024-07-10 12:02:15 +0400884
885func generatePassword() string {
886 return "foo"
887}
giocb34ad22024-07-11 08:01:13 +0400888
gio11617ac2024-07-15 16:09:04 +0400889func (s *DodoAppServer) getNetworks(user string) ([]installer.Network, error) {
gio23bdc1b2024-07-11 16:07:47 +0400890 addr := fmt.Sprintf("%s/api/networks", s.envAppManagerAddr)
giocb34ad22024-07-11 08:01:13 +0400891 resp, err := http.Get(addr)
892 if err != nil {
893 return nil, err
894 }
gio23bdc1b2024-07-11 16:07:47 +0400895 networks := []installer.Network{}
896 if json.NewDecoder(resp.Body).Decode(&networks); err != nil {
giocb34ad22024-07-11 08:01:13 +0400897 return nil, err
898 }
gio11617ac2024-07-15 16:09:04 +0400899 return s.nf.Filter(user, networks)
900}
901
gio8fae3af2024-07-25 13:43:31 +0400902type publicNetworkData struct {
903 Name string `json:"name"`
904 Domain string `json:"domain"`
905}
906
907type publicData struct {
908 Networks []publicNetworkData `json:"networks"`
909 Types []string `json:"types"`
910}
911
912func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
giod8ab4f52024-07-26 16:58:34 +0400913 w.Header().Set("Access-Control-Allow-Origin", "*")
914 s.l.Lock()
915 defer s.l.Unlock()
gio8fae3af2024-07-25 13:43:31 +0400916 networks, err := s.getNetworks("")
917 if err != nil {
918 http.Error(w, err.Error(), http.StatusInternalServerError)
919 return
920 }
921 var ret publicData
922 for _, n := range networks {
giod8ab4f52024-07-26 16:58:34 +0400923 if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
924 ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
925 }
gio8fae3af2024-07-25 13:43:31 +0400926 }
927 for _, t := range s.appTmpls.Types() {
928 ret.Types = append(ret.Types, strings.ReplaceAll(t, "-", ":"))
929 }
930 w.Header().Set("Access-Control-Allow-Origin", "*")
931 if err := json.NewEncoder(w).Encode(ret); err != nil {
932 http.Error(w, err.Error(), http.StatusInternalServerError)
933 return
934 }
935}
936
gio11617ac2024-07-15 16:09:04 +0400937func pickNetwork(networks []installer.Network, network string) []installer.Network {
938 for _, n := range networks {
939 if n.Name == network {
940 return []installer.Network{n}
941 }
942 }
943 return []installer.Network{}
944}
945
946type NetworkFilter interface {
947 Filter(user string, networks []installer.Network) ([]installer.Network, error)
948}
949
950type noNetworkFilter struct{}
951
952func NewNoNetworkFilter() NetworkFilter {
953 return noNetworkFilter{}
954}
955
gio8fae3af2024-07-25 13:43:31 +0400956func (f noNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400957 return networks, nil
958}
959
960type filterByOwner struct {
961 st Store
962}
963
964func NewNetworkFilterByOwner(st Store) NetworkFilter {
965 return &filterByOwner{st}
966}
967
968func (f *filterByOwner) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio8fae3af2024-07-25 13:43:31 +0400969 if user == "" {
970 return networks, nil
971 }
gio11617ac2024-07-15 16:09:04 +0400972 network, err := f.st.GetUserNetwork(user)
973 if err != nil {
974 return nil, err
gio23bdc1b2024-07-11 16:07:47 +0400975 }
976 ret := []installer.Network{}
977 for _, n := range networks {
gio11617ac2024-07-15 16:09:04 +0400978 if n.Name == network {
gio23bdc1b2024-07-11 16:07:47 +0400979 ret = append(ret, n)
980 }
981 }
giocb34ad22024-07-11 08:01:13 +0400982 return ret, nil
983}
gio11617ac2024-07-15 16:09:04 +0400984
985type allowListFilter struct {
986 allowed []string
987}
988
989func NewAllowListFilter(allowed []string) NetworkFilter {
990 return &allowListFilter{allowed}
991}
992
gio8fae3af2024-07-25 13:43:31 +0400993func (f *allowListFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +0400994 ret := []installer.Network{}
995 for _, n := range networks {
996 if slices.Contains(f.allowed, n.Name) {
997 ret = append(ret, n)
998 }
999 }
1000 return ret, nil
1001}
1002
1003type combinedNetworkFilter struct {
1004 filters []NetworkFilter
1005}
1006
1007func NewCombinedFilter(filters ...NetworkFilter) NetworkFilter {
1008 return &combinedNetworkFilter{filters}
1009}
1010
gio8fae3af2024-07-25 13:43:31 +04001011func (f *combinedNetworkFilter) Filter(user string, networks []installer.Network) ([]installer.Network, error) {
gio11617ac2024-07-15 16:09:04 +04001012 ret := networks
1013 var err error
1014 for _, f := range f.filters {
gio8fae3af2024-07-25 13:43:31 +04001015 ret, err = f.Filter(user, ret)
gio11617ac2024-07-15 16:09:04 +04001016 if err != nil {
1017 return nil, err
1018 }
1019 }
1020 return ret, nil
1021}