blob: 808bf7eec93ddf4dc70c011901db8e7d1bf41f8f [file] [log] [blame]
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04001package welcome
2
3import (
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +04004 "context"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04005 "embed"
6 "encoding/json"
giof6ad2982024-08-23 17:42:49 +04007 "errors"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04008 "fmt"
9 "html/template"
10 "io/ioutil"
11 "log"
giof6ad2982024-08-23 17:42:49 +040012 "net"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040013 "net/http"
giof6ad2982024-08-23 17:42:49 +040014 "strconv"
15 "strings"
16 "sync"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040017 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040018
19 "github.com/Masterminds/sprig/v3"
gioaa0fcdb2024-06-10 22:19:25 +040020 "github.com/gorilla/mux"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040021
22 "github.com/giolekva/pcloud/core/installer"
giof6ad2982024-08-23 17:42:49 +040023 "github.com/giolekva/pcloud/core/installer/cluster"
24 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040025 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040026)
27
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040028//go:embed appmanager-tmpl/*
29var appTmpls embed.FS
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040030
giof6ad2982024-08-23 17:42:49 +040031type taskForward struct {
32 task tasks.Task
33 redirectTo string
34}
35
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040036type AppManagerServer struct {
giof6ad2982024-08-23 17:42:49 +040037 l sync.Locker
38 port int
39 repo soft.RepoIO
40 m *installer.AppManager
41 r installer.AppRepository
42 fr installer.AppRepository
43 reconciler *tasks.FixedReconciler
44 h installer.HelmReleaseMonitor
45 cnc installer.ClusterNetworkConfigurator
46 vpnAPIClient installer.VPNAPIClient
47 tasks map[string]taskForward
48 ta map[string]installer.EnvApp
49 tmpl tmplts
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040050}
51
52type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040053 index *template.Template
54 app *template.Template
55 allClusters *template.Template
56 cluster *template.Template
57 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040058}
59
60func parseTemplatesAppManager(fs embed.FS) (tmplts, error) {
61 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "appmanager-tmpl/base.html")
62 if err != nil {
63 return tmplts{}, err
64 }
65 parse := func(path string) (*template.Template, error) {
66 if b, err := base.Clone(); err != nil {
67 return nil, err
68 } else {
69 return b.ParseFS(fs, path)
70 }
71 }
72 index, err := parse("appmanager-tmpl/index.html")
73 if err != nil {
74 return tmplts{}, err
75 }
76 app, err := parse("appmanager-tmpl/app.html")
77 if err != nil {
78 return tmplts{}, err
79 }
giof6ad2982024-08-23 17:42:49 +040080 allClusters, err := parse("appmanager-tmpl/all-clusters.html")
81 if err != nil {
82 return tmplts{}, err
83 }
84 cluster, err := parse("appmanager-tmpl/cluster.html")
85 if err != nil {
86 return tmplts{}, err
87 }
88 task, err := parse("appmanager-tmpl/task.html")
89 if err != nil {
90 return tmplts{}, err
91 }
92 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040093}
94
95func NewAppManagerServer(
96 port int,
giof6ad2982024-08-23 17:42:49 +040097 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040098 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040099 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400100 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400101 reconciler *tasks.FixedReconciler,
gio778577f2024-04-29 09:44:38 +0400102 h installer.HelmReleaseMonitor,
giof6ad2982024-08-23 17:42:49 +0400103 cnc installer.ClusterNetworkConfigurator,
104 vpnAPIClient installer.VPNAPIClient,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400105) (*AppManagerServer, error) {
106 tmpl, err := parseTemplatesAppManager(appTmpls)
107 if err != nil {
108 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400109 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400110 return &AppManagerServer{
giof6ad2982024-08-23 17:42:49 +0400111 l: &sync.Mutex{},
112 port: port,
113 repo: repo,
114 m: m,
115 r: r,
116 fr: fr,
117 reconciler: reconciler,
118 h: h,
119 cnc: cnc,
120 vpnAPIClient: vpnAPIClient,
121 tasks: make(map[string]taskForward),
122 ta: make(map[string]installer.EnvApp),
123 tmpl: tmpl,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400124 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400125}
126
gio09f8efa2024-06-10 22:35:24 +0400127type cachingHandler struct {
128 h http.Handler
129}
130
131func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
132 w.Header().Set("Cache-Control", "max-age=604800")
133 h.h.ServeHTTP(w, r)
134}
135
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400136func (s *AppManagerServer) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400137 r := mux.NewRouter()
gio1bf00802024-08-17 12:31:41 +0400138 r.PathPrefix("/stat/").Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
giocb34ad22024-07-11 08:01:13 +0400139 r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
giof15b9da2024-09-19 06:59:16 +0400140 r.HandleFunc("/api/clusters", s.handleClusters).Methods(http.MethodGet)
141 r.HandleFunc("/api/proxy/add", s.handleProxyAdd).Methods(http.MethodPost)
142 r.HandleFunc("/api/proxy/remove", s.handleProxyRemove).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400143 r.HandleFunc("/api/app-repo", s.handleAppRepo)
144 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
145 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
146 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
147 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
148 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400149 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
150 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
151 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
152 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
153 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
154 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400155 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
156 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400157 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400158 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
159 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400160 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400161 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400162}
163
giocb34ad22024-07-11 08:01:13 +0400164func (s *AppManagerServer) handleNetworks(w http.ResponseWriter, r *http.Request) {
165 env, err := s.m.Config()
166 if err != nil {
167 http.Error(w, err.Error(), http.StatusInternalServerError)
168 return
169 }
170 networks, err := s.m.CreateNetworks(env)
171 if err != nil {
172 http.Error(w, err.Error(), http.StatusInternalServerError)
173 return
174 }
175 if err := json.NewEncoder(w).Encode(networks); err != nil {
176 http.Error(w, err.Error(), http.StatusInternalServerError)
177 return
178 }
179}
180
giof15b9da2024-09-19 06:59:16 +0400181func (s *AppManagerServer) handleClusters(w http.ResponseWriter, r *http.Request) {
182 clusters, err := s.m.GetClusters()
183 if err != nil {
184 http.Error(w, err.Error(), http.StatusInternalServerError)
185 return
186 }
187 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
188 http.Error(w, err.Error(), http.StatusInternalServerError)
189 return
190 }
191}
192
193type proxyPair struct {
194 From string `json:"from"`
195 To string `json:"to"`
196}
197
198func (s *AppManagerServer) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
199 var req proxyPair
200 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
201 http.Error(w, err.Error(), http.StatusBadRequest)
202 return
203 }
204 if err := s.cnc.AddProxy(req.From, req.To); err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 }
208}
209
210func (s *AppManagerServer) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
211 var req proxyPair
212 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
213 http.Error(w, err.Error(), http.StatusBadRequest)
214 return
215 }
216 if err := s.cnc.RemoveProxy(req.From, req.To); err != nil {
217 http.Error(w, err.Error(), http.StatusInternalServerError)
218 return
219 }
220}
221
222type app struct {
223 Name string `json:"name"`
224 Icon template.HTML `json:"icon"`
225 ShortDescription string `json:"shortDescription"`
226 Slug string `json:"slug"`
227 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
228}
229
gioaa0fcdb2024-06-10 22:19:25 +0400230func (s *AppManagerServer) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400231 all, err := s.r.GetAll()
232 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400233 http.Error(w, err.Error(), http.StatusInternalServerError)
234 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400235 }
236 resp := make([]app, len(all))
237 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400238 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400239 }
gioaa0fcdb2024-06-10 22:19:25 +0400240 w.Header().Set("Content-Type", "application/json")
241 if err := json.NewEncoder(w).Encode(resp); err != nil {
242 http.Error(w, err.Error(), http.StatusInternalServerError)
243 return
244 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400245}
246
gioaa0fcdb2024-06-10 22:19:25 +0400247func (s *AppManagerServer) handleApp(w http.ResponseWriter, r *http.Request) {
248 slug, ok := mux.Vars(r)["slug"]
249 if !ok {
250 http.Error(w, "empty slug", http.StatusBadRequest)
251 return
252 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400253 a, err := s.r.Find(slug)
254 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400257 }
gio7fbd4ad2024-08-27 10:06:39 +0400258 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400259 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400260 http.Error(w, err.Error(), http.StatusInternalServerError)
261 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400262 }
gioaa0fcdb2024-06-10 22:19:25 +0400263 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
264 w.Header().Set("Content-Type", "application/json")
265 if err := json.NewEncoder(w).Encode(resp); err != nil {
266 http.Error(w, err.Error(), http.StatusInternalServerError)
267 return
268 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400269}
270
gioaa0fcdb2024-06-10 22:19:25 +0400271func (s *AppManagerServer) handleInstance(w http.ResponseWriter, r *http.Request) {
272 slug, ok := mux.Vars(r)["slug"]
273 if !ok {
274 http.Error(w, "empty slug", http.StatusBadRequest)
275 return
276 }
gio7fbd4ad2024-08-27 10:06:39 +0400277 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400278 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400279 http.Error(w, err.Error(), http.StatusInternalServerError)
280 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400281 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400282 a, err := s.r.Find(instance.AppId)
283 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400284 http.Error(w, err.Error(), http.StatusInternalServerError)
285 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400286 }
gioaa0fcdb2024-06-10 22:19:25 +0400287 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
288 w.Header().Set("Content-Type", "application/json")
289 if err := json.NewEncoder(w).Encode(resp); err != nil {
290 http.Error(w, err.Error(), http.StatusInternalServerError)
291 return
292 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400293}
294
gioaa0fcdb2024-06-10 22:19:25 +0400295func (s *AppManagerServer) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400296 s.l.Lock()
297 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400298 slug, ok := mux.Vars(r)["slug"]
299 if !ok {
300 http.Error(w, "empty slug", http.StatusBadRequest)
301 return
302 }
303 contents, err := ioutil.ReadAll(r.Body)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400304 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400305 http.Error(w, err.Error(), http.StatusInternalServerError)
306 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400307 }
308 var values map[string]any
309 if err := json.Unmarshal(contents, &values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400310 http.Error(w, err.Error(), http.StatusInternalServerError)
311 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400312 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400313 log.Printf("Values: %+v\n", values)
gio3cdee592024-04-17 10:15:56 +0400314 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400315 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400316 http.Error(w, err.Error(), http.StatusInternalServerError)
317 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400319 log.Printf("Found application: %s\n", slug)
gio3cdee592024-04-17 10:15:56 +0400320 env, err := s.m.Config()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400321 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400322 http.Error(w, err.Error(), http.StatusInternalServerError)
323 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400324 }
gio3cdee592024-04-17 10:15:56 +0400325 log.Printf("Configuration: %+v\n", env)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400326 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400327 suffix, err := suffixGen.Generate()
328 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400329 http.Error(w, err.Error(), http.StatusInternalServerError)
330 return
gio3af43942024-04-16 08:13:50 +0400331 }
gio44f621b2024-04-29 09:44:38 +0400332 instanceId := a.Slug() + suffix
gio3cdee592024-04-17 10:15:56 +0400333 appDir := fmt.Sprintf("/apps/%s", instanceId)
334 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
gio1cd65152024-08-16 08:18:49 +0400335 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gio43b0f422024-08-21 10:40:13 +0400336 rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
337 if err == nil {
338 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
339 go s.reconciler.Reconcile(ctx)
340 }
341 return rr, err
gio1cd65152024-08-16 08:18:49 +0400342 })
gio778577f2024-04-29 09:44:38 +0400343 if _, ok := s.tasks[instanceId]; ok {
344 panic("MUST NOT REACH!")
345 }
giof6ad2982024-08-23 17:42:49 +0400346 s.tasks[instanceId] = taskForward{t, fmt.Sprintf("/instance/%s", instanceId)}
gio1cd65152024-08-16 08:18:49 +0400347 s.ta[instanceId] = a
gio778577f2024-04-29 09:44:38 +0400348 t.OnDone(func(err error) {
giof6ad2982024-08-23 17:42:49 +0400349 go func() {
350 time.Sleep(30 * time.Second)
351 s.l.Lock()
352 defer s.l.Unlock()
353 delete(s.tasks, instanceId)
354 delete(s.ta, instanceId)
355 }()
gio778577f2024-04-29 09:44:38 +0400356 })
gio778577f2024-04-29 09:44:38 +0400357 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400358 if _, err := fmt.Fprintf(w, "/tasks/%s", instanceId); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
361 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400362}
363
gioaa0fcdb2024-06-10 22:19:25 +0400364func (s *AppManagerServer) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400365 s.l.Lock()
366 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400367 slug, ok := mux.Vars(r)["slug"]
368 if !ok {
369 http.Error(w, "empty slug", http.StatusBadRequest)
370 return
371 }
gioaa0fcdb2024-06-10 22:19:25 +0400372 contents, err := ioutil.ReadAll(r.Body)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400373 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400374 http.Error(w, err.Error(), http.StatusInternalServerError)
375 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400376 }
377 var values map[string]any
378 if err := json.Unmarshal(contents, &values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400381 }
gio778577f2024-04-29 09:44:38 +0400382 if _, ok := s.tasks[slug]; ok {
gioaa0fcdb2024-06-10 22:19:25 +0400383 http.Error(w, "Update already in progress", http.StatusBadRequest)
384 return
gio778577f2024-04-29 09:44:38 +0400385 }
giof8843412024-05-22 16:38:05 +0400386 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400387 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400388 http.Error(w, err.Error(), http.StatusInternalServerError)
389 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400390 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400391 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
392 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400393 t := tasks.NewMonitorRelease(s.h, rr)
394 t.OnDone(func(err error) {
giof6ad2982024-08-23 17:42:49 +0400395 go func() {
396 time.Sleep(30 * time.Second)
397 s.l.Lock()
398 defer s.l.Unlock()
399 delete(s.tasks, slug)
400 }()
gio778577f2024-04-29 09:44:38 +0400401 })
giof6ad2982024-08-23 17:42:49 +0400402 s.tasks[slug] = taskForward{t, fmt.Sprintf("/instance/%s", slug)}
gio778577f2024-04-29 09:44:38 +0400403 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400404 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400405 http.Error(w, err.Error(), http.StatusInternalServerError)
406 return
407 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400408}
409
gioaa0fcdb2024-06-10 22:19:25 +0400410func (s *AppManagerServer) handleAppRemove(w http.ResponseWriter, r *http.Request) {
411 slug, ok := mux.Vars(r)["slug"]
412 if !ok {
413 http.Error(w, "empty slug", http.StatusBadRequest)
414 return
415 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400416 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400417 http.Error(w, err.Error(), http.StatusInternalServerError)
418 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400419 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400420 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
421 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400422 if _, err := fmt.Fprint(w, "/"); err != nil {
423 http.Error(w, err.Error(), http.StatusInternalServerError)
424 return
425 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400426}
427
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400428type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400429 Apps []app
430 CurrentPage string
431 SearchTarget string
432 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400433}
434
Davit Tabidze780a0d02024-08-05 20:53:26 +0400435func (s *AppManagerServer) handleAppsList(w http.ResponseWriter, r *http.Request) {
436 pageType := mux.Vars(r)["pageType"]
437 if pageType == "" {
438 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400439 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400440 searchQuery := r.FormValue("query")
441 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400442 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400443 http.Error(w, err.Error(), http.StatusInternalServerError)
444 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400445 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400446 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400447 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400448 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400449 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400450 http.Error(w, err.Error(), http.StatusInternalServerError)
451 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400452 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400453 switch pageType {
454 case "installed":
455 if len(instances) != 0 {
456 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
457 }
458 case "not-installed":
459 if len(instances) == 0 {
460 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
461 }
462 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400463 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
464 }
465 }
466 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400467 Apps: resp,
468 CurrentPage: pageType,
469 SearchTarget: pageType,
470 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400471 }
gioaa0fcdb2024-06-10 22:19:25 +0400472 if err := s.tmpl.index.Execute(w, data); err != nil {
473 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400474 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400475}
476
477type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400478 App installer.EnvApp
479 Instance *installer.AppInstanceConfig
480 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400481 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400482 AvailableClusters []cluster.State
gio778577f2024-04-29 09:44:38 +0400483 Task tasks.Task
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400484 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400485}
486
gioaa0fcdb2024-06-10 22:19:25 +0400487func (s *AppManagerServer) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400488 global, err := s.m.Config()
489 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400490 http.Error(w, err.Error(), http.StatusInternalServerError)
491 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400492 }
gioaa0fcdb2024-06-10 22:19:25 +0400493 slug, ok := mux.Vars(r)["slug"]
494 if !ok {
495 http.Error(w, "empty slug", http.StatusBadRequest)
496 return
497 }
gio3cdee592024-04-17 10:15:56 +0400498 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400499 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400500 http.Error(w, err.Error(), http.StatusInternalServerError)
501 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400502 }
gio7fbd4ad2024-08-27 10:06:39 +0400503 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400504 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400505 http.Error(w, err.Error(), http.StatusInternalServerError)
506 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400507 }
giocb34ad22024-07-11 08:01:13 +0400508 networks, err := s.m.CreateNetworks(global)
509 if err != nil {
510 http.Error(w, err.Error(), http.StatusInternalServerError)
511 return
512 }
giof6ad2982024-08-23 17:42:49 +0400513 clusters, err := s.m.GetClusters()
514 if err != nil {
515 http.Error(w, err.Error(), http.StatusInternalServerError)
516 return
517 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400518 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400519 App: a,
520 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400521 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400522 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400523 CurrentPage: a.Name(),
524 }
gioaa0fcdb2024-06-10 22:19:25 +0400525 if err := s.tmpl.app.Execute(w, data); err != nil {
526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
528 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400529}
530
gioaa0fcdb2024-06-10 22:19:25 +0400531func (s *AppManagerServer) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400532 s.l.Lock()
533 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400534 global, err := s.m.Config()
535 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400536 http.Error(w, err.Error(), http.StatusInternalServerError)
537 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400538 }
gioaa0fcdb2024-06-10 22:19:25 +0400539 slug, ok := mux.Vars(r)["slug"]
540 if !ok {
541 http.Error(w, "empty slug", http.StatusBadRequest)
542 return
543 }
gio1cd65152024-08-16 08:18:49 +0400544 t, ok := s.tasks[slug]
gio7fbd4ad2024-08-27 10:06:39 +0400545 instance, err := s.m.GetInstance(slug)
gio1cd65152024-08-16 08:18:49 +0400546 if err != nil && !ok {
gioaa0fcdb2024-06-10 22:19:25 +0400547 http.Error(w, err.Error(), http.StatusInternalServerError)
548 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400549 }
giof6ad2982024-08-23 17:42:49 +0400550 if ok && !(t.task.Status() == tasks.StatusDone || t.task.Status() == tasks.StatusFailed) {
551 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
552 return
553 }
gio1cd65152024-08-16 08:18:49 +0400554 var a installer.EnvApp
555 if instance != nil {
556 a, err = s.m.GetInstanceApp(instance.Id)
557 if err != nil {
558 http.Error(w, err.Error(), http.StatusInternalServerError)
559 return
560 }
561 } else {
562 var ok bool
563 a, ok = s.ta[slug]
564 if !ok {
565 panic("MUST NOT REACH!")
566 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400567 }
gio7fbd4ad2024-08-27 10:06:39 +0400568 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400569 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400570 http.Error(w, err.Error(), http.StatusInternalServerError)
571 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400572 }
giocb34ad22024-07-11 08:01:13 +0400573 networks, err := s.m.CreateNetworks(global)
574 if err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
giof6ad2982024-08-23 17:42:49 +0400578 clusters, err := s.m.GetClusters()
579 if err != nil {
580 http.Error(w, err.Error(), http.StatusInternalServerError)
581 return
582 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400583 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400584 App: a,
gio778577f2024-04-29 09:44:38 +0400585 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400586 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400587 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400588 AvailableClusters: clusters,
589 Task: t.task,
gio1cd65152024-08-16 08:18:49 +0400590 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400591 }
gioaa0fcdb2024-06-10 22:19:25 +0400592 if err := s.tmpl.app.Execute(w, data); err != nil {
593 http.Error(w, err.Error(), http.StatusInternalServerError)
594 return
595 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400596}
giof6ad2982024-08-23 17:42:49 +0400597
598type taskStatusData struct {
599 CurrentPage string
600 Task tasks.Task
601}
602
603func (s *AppManagerServer) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
604 s.l.Lock()
605 defer s.l.Unlock()
606 slug, ok := mux.Vars(r)["slug"]
607 if !ok {
608 http.Error(w, "empty slug", http.StatusBadRequest)
609 return
610 }
611 t, ok := s.tasks[slug]
612 if !ok {
613 http.Error(w, "task not found", http.StatusInternalServerError)
614
615 return
616 }
617 if ok && (t.task.Status() == tasks.StatusDone || t.task.Status() == tasks.StatusFailed) {
618 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
619 return
620 }
621 data := taskStatusData{
622 CurrentPage: "",
623 Task: t.task,
624 }
625 if err := s.tmpl.task.Execute(w, data); err != nil {
626 http.Error(w, err.Error(), http.StatusInternalServerError)
627 return
628 }
629}
630
631type clustersData struct {
632 CurrentPage string
633 Clusters []cluster.State
634}
635
636func (s *AppManagerServer) handleAllClusters(w http.ResponseWriter, r *http.Request) {
637 clusters, err := s.m.GetClusters()
638 if err != nil {
639 http.Error(w, err.Error(), http.StatusInternalServerError)
640 return
641 }
642 data := clustersData{
643 "clusters",
644 clusters,
645 }
646 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
650}
651
652type clusterData struct {
653 CurrentPage string
654 Cluster cluster.State
655}
656
657func (s *AppManagerServer) handleCluster(w http.ResponseWriter, r *http.Request) {
658 name, ok := mux.Vars(r)["name"]
659 if !ok {
660 http.Error(w, "empty name", http.StatusBadRequest)
661 return
662 }
663 m, err := s.getClusterManager(name)
664 if err != nil {
665 if errors.Is(err, installer.ErrorNotFound) {
666 http.Error(w, "not found", http.StatusNotFound)
667 } else {
668 http.Error(w, err.Error(), http.StatusInternalServerError)
669 }
670 return
671 }
672 data := clusterData{
673 "clusters",
674 m.State(),
675 }
676 if err := s.tmpl.cluster.Execute(w, data); err != nil {
677 http.Error(w, err.Error(), http.StatusInternalServerError)
678 return
679 }
680}
681
682func (s *AppManagerServer) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
683 s.l.Lock()
684 defer s.l.Unlock()
685 cName, ok := mux.Vars(r)["cluster"]
686 if !ok {
687 http.Error(w, "empty name", http.StatusBadRequest)
688 return
689 }
690 if _, ok := s.tasks[cName]; ok {
691 http.Error(w, "cluster task in progress", http.StatusLocked)
692 return
693 }
694 sName, ok := mux.Vars(r)["server"]
695 if !ok {
696 http.Error(w, "empty name", http.StatusBadRequest)
697 return
698 }
699 m, err := s.getClusterManager(cName)
700 if err != nil {
701 if errors.Is(err, installer.ErrorNotFound) {
702 http.Error(w, "not found", http.StatusNotFound)
703 } else {
704 http.Error(w, err.Error(), http.StatusInternalServerError)
705 }
706 return
707 }
708 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
709 task.OnDone(func(err error) {
710 go func() {
711 time.Sleep(30 * time.Second)
712 s.l.Lock()
713 defer s.l.Unlock()
714 delete(s.tasks, cName)
715 }()
716 })
717 go task.Start()
718 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
719 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
720}
721
722func (s *AppManagerServer) getClusterManager(cName string) (cluster.Manager, error) {
723 clusters, err := s.m.GetClusters()
724 if err != nil {
725 return nil, err
726 }
727 var c *cluster.State
728 for _, i := range clusters {
729 if i.Name == cName {
730 c = &i
731 break
732 }
733 }
734 if c == nil {
735 return nil, installer.ErrorNotFound
736 }
737 return cluster.RestoreKubeManager(*c)
738}
739
740func (s *AppManagerServer) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
741 s.l.Lock()
742 defer s.l.Unlock()
743 cName, ok := mux.Vars(r)["cluster"]
744 if !ok {
745 http.Error(w, "empty name", http.StatusBadRequest)
746 return
747 }
748 if _, ok := s.tasks[cName]; ok {
749 http.Error(w, "cluster task in progress", http.StatusLocked)
750 return
751 }
752 m, err := s.getClusterManager(cName)
753 if err != nil {
754 if errors.Is(err, installer.ErrorNotFound) {
755 http.Error(w, "not found", http.StatusNotFound)
756 } else {
757 http.Error(w, err.Error(), http.StatusInternalServerError)
758 }
759 return
760 }
761 t := r.PostFormValue("type")
762 ip := net.ParseIP(r.PostFormValue("ip"))
763 if ip == nil {
764 http.Error(w, "invalid ip", http.StatusBadRequest)
765 return
766 }
767 port := 22
768 if p := r.PostFormValue("port"); p != "" {
769 port, err = strconv.Atoi(p)
770 if err != nil {
771 http.Error(w, err.Error(), http.StatusBadRequest)
772 return
773 }
774 }
775 server := cluster.Server{
776 IP: ip,
777 Port: port,
778 User: r.PostFormValue("user"),
779 Password: r.PostFormValue("password"),
780 }
781 var task tasks.Task
782 switch strings.ToLower(t) {
783 case "controller":
784 if len(m.State().Controllers) == 0 {
785 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
786 } else {
787 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
788 }
789 case "worker":
790 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
791 default:
792 http.Error(w, "invalid type", http.StatusBadRequest)
793 return
794 }
795 task.OnDone(func(err error) {
796 go func() {
797 time.Sleep(30 * time.Second)
798 s.l.Lock()
799 defer s.l.Unlock()
800 delete(s.tasks, cName)
801 }()
802 })
803 go task.Start()
804 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
805 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
806}
807
808func (s *AppManagerServer) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
809 cName := r.PostFormValue("name")
810 if cName == "" {
811 http.Error(w, "no name", http.StatusBadRequest)
812 return
813 }
814 st := cluster.State{Name: cName}
815 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
816 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
817 return "", err
818 }
819 return fmt.Sprintf("create cluster: %s", cName), nil
820 }); err != nil {
821 http.Error(w, err.Error(), http.StatusInternalServerError)
822 return
823 }
824 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
825}
826
827func (s *AppManagerServer) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
828 cName, ok := mux.Vars(r)["name"]
829 if !ok {
830 http.Error(w, "empty name", http.StatusBadRequest)
831 return
832 }
833 if _, ok := s.tasks[cName]; ok {
834 http.Error(w, "cluster task in progress", http.StatusLocked)
835 return
836 }
837 m, err := s.getClusterManager(cName)
838 if err != nil {
839 if errors.Is(err, installer.ErrorNotFound) {
840 http.Error(w, "not found", http.StatusNotFound)
841 } else {
842 http.Error(w, err.Error(), http.StatusInternalServerError)
843 }
844 return
845 }
846 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
847 task.OnDone(func(err error) {
848 go func() {
849 time.Sleep(30 * time.Second)
850 s.l.Lock()
851 defer s.l.Unlock()
852 delete(s.tasks, cName)
853 }()
854 })
855 go task.Start()
856 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
857 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
858}
859
860func (s *AppManagerServer) setupRemoteCluster() cluster.ClusterSetupFunc {
861 const vpnUser = "private-network-proxy"
862 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
863 hostname := fmt.Sprintf("cluster-%s", name)
864 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
865 app, err := installer.FindEnvApp(s.fr, "cluster-network")
866 if err != nil {
867 return installer.ReleaseResources{}, err
868 }
869 env, err := s.m.Config()
870 if err != nil {
871 return installer.ReleaseResources{}, err
872 }
873 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
874 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
875 namespace := fmt.Sprintf("%scluster-network-%s", env.NamespacePrefix, name)
876 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
877 "cluster": map[string]any{
878 "name": name,
879 "kubeconfig": kubeconfig,
880 "ingressClassName": ingressClassName,
881 },
882 // TODO(gio): remove hardcoded user
883 "vpnUser": vpnUser,
884 "vpnProxyHostname": hostname,
885 })
886 if err != nil {
887 return installer.ReleaseResources{}, err
888 }
889 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
890 go s.reconciler.Reconcile(ctx)
891 return rr, err
892 })
893 ch := make(chan error)
894 t.OnDone(func(err error) {
895 ch <- err
896 })
897 go t.Start()
898 err := <-ch
899 if err != nil {
900 return nil, err
901 }
902 for {
903 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
904 if err == nil {
905 return ip, nil
906 }
907 if errors.Is(err, installer.ErrorNotFound) {
908 time.Sleep(5 * time.Second)
909 }
910 }
911 }
912}