blob: 30161903e1d663140f0eb78a09ebbc78641c5100 [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)
gioaa0fcdb2024-06-10 22:19:25 +0400140 r.HandleFunc("/api/app-repo", s.handleAppRepo)
141 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
142 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
143 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
144 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
145 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400146 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
147 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
148 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
149 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
150 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
151 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400152 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
153 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400154 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400155 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
156 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400157 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400158 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400159}
160
161type app struct {
gio3cdee592024-04-17 10:15:56 +0400162 Name string `json:"name"`
163 Icon template.HTML `json:"icon"`
164 ShortDescription string `json:"shortDescription"`
165 Slug string `json:"slug"`
166 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400167}
168
giocb34ad22024-07-11 08:01:13 +0400169func (s *AppManagerServer) handleNetworks(w http.ResponseWriter, r *http.Request) {
170 env, err := s.m.Config()
171 if err != nil {
172 http.Error(w, err.Error(), http.StatusInternalServerError)
173 return
174 }
175 networks, err := s.m.CreateNetworks(env)
176 if err != nil {
177 http.Error(w, err.Error(), http.StatusInternalServerError)
178 return
179 }
180 if err := json.NewEncoder(w).Encode(networks); err != nil {
181 http.Error(w, err.Error(), http.StatusInternalServerError)
182 return
183 }
184}
185
gioaa0fcdb2024-06-10 22:19:25 +0400186func (s *AppManagerServer) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400187 all, err := s.r.GetAll()
188 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400189 http.Error(w, err.Error(), http.StatusInternalServerError)
190 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400191 }
192 resp := make([]app, len(all))
193 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400194 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400195 }
gioaa0fcdb2024-06-10 22:19:25 +0400196 w.Header().Set("Content-Type", "application/json")
197 if err := json.NewEncoder(w).Encode(resp); err != nil {
198 http.Error(w, err.Error(), http.StatusInternalServerError)
199 return
200 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400201}
202
gioaa0fcdb2024-06-10 22:19:25 +0400203func (s *AppManagerServer) handleApp(w http.ResponseWriter, r *http.Request) {
204 slug, ok := mux.Vars(r)["slug"]
205 if !ok {
206 http.Error(w, "empty slug", http.StatusBadRequest)
207 return
208 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400209 a, err := s.r.Find(slug)
210 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400211 http.Error(w, err.Error(), http.StatusInternalServerError)
212 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400213 }
gio7fbd4ad2024-08-27 10:06:39 +0400214 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400215 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400218 }
gioaa0fcdb2024-06-10 22:19:25 +0400219 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
220 w.Header().Set("Content-Type", "application/json")
221 if err := json.NewEncoder(w).Encode(resp); err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
224 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400225}
226
gioaa0fcdb2024-06-10 22:19:25 +0400227func (s *AppManagerServer) handleInstance(w http.ResponseWriter, r *http.Request) {
228 slug, ok := mux.Vars(r)["slug"]
229 if !ok {
230 http.Error(w, "empty slug", http.StatusBadRequest)
231 return
232 }
gio7fbd4ad2024-08-27 10:06:39 +0400233 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400234 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400235 http.Error(w, err.Error(), http.StatusInternalServerError)
236 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400237 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400238 a, err := s.r.Find(instance.AppId)
239 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400240 http.Error(w, err.Error(), http.StatusInternalServerError)
241 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400242 }
gioaa0fcdb2024-06-10 22:19:25 +0400243 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
244 w.Header().Set("Content-Type", "application/json")
245 if err := json.NewEncoder(w).Encode(resp); err != nil {
246 http.Error(w, err.Error(), http.StatusInternalServerError)
247 return
248 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400249}
250
gioaa0fcdb2024-06-10 22:19:25 +0400251func (s *AppManagerServer) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400252 s.l.Lock()
253 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400254 slug, ok := mux.Vars(r)["slug"]
255 if !ok {
256 http.Error(w, "empty slug", http.StatusBadRequest)
257 return
258 }
259 contents, err := ioutil.ReadAll(r.Body)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400260 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400261 http.Error(w, err.Error(), http.StatusInternalServerError)
262 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400263 }
264 var values map[string]any
265 if err := json.Unmarshal(contents, &values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400266 http.Error(w, err.Error(), http.StatusInternalServerError)
267 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400268 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400269 log.Printf("Values: %+v\n", values)
gio3cdee592024-04-17 10:15:56 +0400270 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400271 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400274 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400275 log.Printf("Found application: %s\n", slug)
gio3cdee592024-04-17 10:15:56 +0400276 env, err := s.m.Config()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400277 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400278 http.Error(w, err.Error(), http.StatusInternalServerError)
279 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400280 }
gio3cdee592024-04-17 10:15:56 +0400281 log.Printf("Configuration: %+v\n", env)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400282 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400283 suffix, err := suffixGen.Generate()
284 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400285 http.Error(w, err.Error(), http.StatusInternalServerError)
286 return
gio3af43942024-04-16 08:13:50 +0400287 }
gio44f621b2024-04-29 09:44:38 +0400288 instanceId := a.Slug() + suffix
gio3cdee592024-04-17 10:15:56 +0400289 appDir := fmt.Sprintf("/apps/%s", instanceId)
290 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
gio1cd65152024-08-16 08:18:49 +0400291 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gio43b0f422024-08-21 10:40:13 +0400292 rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
293 if err == nil {
294 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
295 go s.reconciler.Reconcile(ctx)
296 }
297 return rr, err
gio1cd65152024-08-16 08:18:49 +0400298 })
gio778577f2024-04-29 09:44:38 +0400299 if _, ok := s.tasks[instanceId]; ok {
300 panic("MUST NOT REACH!")
301 }
giof6ad2982024-08-23 17:42:49 +0400302 s.tasks[instanceId] = taskForward{t, fmt.Sprintf("/instance/%s", instanceId)}
gio1cd65152024-08-16 08:18:49 +0400303 s.ta[instanceId] = a
gio778577f2024-04-29 09:44:38 +0400304 t.OnDone(func(err error) {
giof6ad2982024-08-23 17:42:49 +0400305 go func() {
306 time.Sleep(30 * time.Second)
307 s.l.Lock()
308 defer s.l.Unlock()
309 delete(s.tasks, instanceId)
310 delete(s.ta, instanceId)
311 }()
gio778577f2024-04-29 09:44:38 +0400312 })
gio778577f2024-04-29 09:44:38 +0400313 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400314 if _, err := fmt.Fprintf(w, "/tasks/%s", instanceId); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400315 http.Error(w, err.Error(), http.StatusInternalServerError)
316 return
317 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318}
319
gioaa0fcdb2024-06-10 22:19:25 +0400320func (s *AppManagerServer) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400321 s.l.Lock()
322 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400323 slug, ok := mux.Vars(r)["slug"]
324 if !ok {
325 http.Error(w, "empty slug", http.StatusBadRequest)
326 return
327 }
gioaa0fcdb2024-06-10 22:19:25 +0400328 contents, err := ioutil.ReadAll(r.Body)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400329 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400330 http.Error(w, err.Error(), http.StatusInternalServerError)
331 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400332 }
333 var values map[string]any
334 if err := json.Unmarshal(contents, &values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400335 http.Error(w, err.Error(), http.StatusInternalServerError)
336 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400337 }
gio778577f2024-04-29 09:44:38 +0400338 if _, ok := s.tasks[slug]; ok {
gioaa0fcdb2024-06-10 22:19:25 +0400339 http.Error(w, "Update already in progress", http.StatusBadRequest)
340 return
gio778577f2024-04-29 09:44:38 +0400341 }
giof8843412024-05-22 16:38:05 +0400342 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400343 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400344 http.Error(w, err.Error(), http.StatusInternalServerError)
345 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400346 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400347 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
348 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400349 t := tasks.NewMonitorRelease(s.h, rr)
350 t.OnDone(func(err error) {
giof6ad2982024-08-23 17:42:49 +0400351 go func() {
352 time.Sleep(30 * time.Second)
353 s.l.Lock()
354 defer s.l.Unlock()
355 delete(s.tasks, slug)
356 }()
gio778577f2024-04-29 09:44:38 +0400357 })
giof6ad2982024-08-23 17:42:49 +0400358 s.tasks[slug] = taskForward{t, fmt.Sprintf("/instance/%s", slug)}
gio778577f2024-04-29 09:44:38 +0400359 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400360 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400361 http.Error(w, err.Error(), http.StatusInternalServerError)
362 return
363 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400364}
365
gioaa0fcdb2024-06-10 22:19:25 +0400366func (s *AppManagerServer) handleAppRemove(w http.ResponseWriter, r *http.Request) {
367 slug, ok := mux.Vars(r)["slug"]
368 if !ok {
369 http.Error(w, "empty slug", http.StatusBadRequest)
370 return
371 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400372 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400375 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400376 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
377 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400378 if _, err := fmt.Fprint(w, "/"); err != nil {
379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
381 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400382}
383
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400384type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400385 Apps []app
386 CurrentPage string
387 SearchTarget string
388 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400389}
390
Davit Tabidze780a0d02024-08-05 20:53:26 +0400391func (s *AppManagerServer) handleAppsList(w http.ResponseWriter, r *http.Request) {
392 pageType := mux.Vars(r)["pageType"]
393 if pageType == "" {
394 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400396 searchQuery := r.FormValue("query")
397 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400398 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400399 http.Error(w, err.Error(), http.StatusInternalServerError)
400 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400401 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400402 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400403 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400404 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400405 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400408 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400409 switch pageType {
410 case "installed":
411 if len(instances) != 0 {
412 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
413 }
414 case "not-installed":
415 if len(instances) == 0 {
416 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
417 }
418 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400419 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
420 }
421 }
422 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400423 Apps: resp,
424 CurrentPage: pageType,
425 SearchTarget: pageType,
426 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400427 }
gioaa0fcdb2024-06-10 22:19:25 +0400428 if err := s.tmpl.index.Execute(w, data); err != nil {
429 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400430 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400431}
432
433type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400434 App installer.EnvApp
435 Instance *installer.AppInstanceConfig
436 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400437 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400438 AvailableClusters []cluster.State
gio778577f2024-04-29 09:44:38 +0400439 Task tasks.Task
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400440 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400441}
442
gioaa0fcdb2024-06-10 22:19:25 +0400443func (s *AppManagerServer) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400444 global, err := s.m.Config()
445 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400446 http.Error(w, err.Error(), http.StatusInternalServerError)
447 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400448 }
gioaa0fcdb2024-06-10 22:19:25 +0400449 slug, ok := mux.Vars(r)["slug"]
450 if !ok {
451 http.Error(w, "empty slug", http.StatusBadRequest)
452 return
453 }
gio3cdee592024-04-17 10:15:56 +0400454 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400455 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400456 http.Error(w, err.Error(), http.StatusInternalServerError)
457 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400458 }
gio7fbd4ad2024-08-27 10:06:39 +0400459 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400460 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400461 http.Error(w, err.Error(), http.StatusInternalServerError)
462 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400463 }
giocb34ad22024-07-11 08:01:13 +0400464 networks, err := s.m.CreateNetworks(global)
465 if err != nil {
466 http.Error(w, err.Error(), http.StatusInternalServerError)
467 return
468 }
giof6ad2982024-08-23 17:42:49 +0400469 clusters, err := s.m.GetClusters()
470 if err != nil {
471 http.Error(w, err.Error(), http.StatusInternalServerError)
472 return
473 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400474 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400475 App: a,
476 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400477 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400478 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400479 CurrentPage: a.Name(),
480 }
gioaa0fcdb2024-06-10 22:19:25 +0400481 if err := s.tmpl.app.Execute(w, data); err != nil {
482 http.Error(w, err.Error(), http.StatusInternalServerError)
483 return
484 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400485}
486
gioaa0fcdb2024-06-10 22:19:25 +0400487func (s *AppManagerServer) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400488 s.l.Lock()
489 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400490 global, err := s.m.Config()
491 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400492 http.Error(w, err.Error(), http.StatusInternalServerError)
493 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400494 }
gioaa0fcdb2024-06-10 22:19:25 +0400495 slug, ok := mux.Vars(r)["slug"]
496 if !ok {
497 http.Error(w, "empty slug", http.StatusBadRequest)
498 return
499 }
gio1cd65152024-08-16 08:18:49 +0400500 t, ok := s.tasks[slug]
gio7fbd4ad2024-08-27 10:06:39 +0400501 instance, err := s.m.GetInstance(slug)
gio1cd65152024-08-16 08:18:49 +0400502 if err != nil && !ok {
gioaa0fcdb2024-06-10 22:19:25 +0400503 http.Error(w, err.Error(), http.StatusInternalServerError)
504 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400505 }
giof6ad2982024-08-23 17:42:49 +0400506 if ok && !(t.task.Status() == tasks.StatusDone || t.task.Status() == tasks.StatusFailed) {
507 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
508 return
509 }
gio1cd65152024-08-16 08:18:49 +0400510 var a installer.EnvApp
511 if instance != nil {
512 a, err = s.m.GetInstanceApp(instance.Id)
513 if err != nil {
514 http.Error(w, err.Error(), http.StatusInternalServerError)
515 return
516 }
517 } else {
518 var ok bool
519 a, ok = s.ta[slug]
520 if !ok {
521 panic("MUST NOT REACH!")
522 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400523 }
gio7fbd4ad2024-08-27 10:06:39 +0400524 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400525 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400526 http.Error(w, err.Error(), http.StatusInternalServerError)
527 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400528 }
giocb34ad22024-07-11 08:01:13 +0400529 networks, err := s.m.CreateNetworks(global)
530 if err != nil {
531 http.Error(w, err.Error(), http.StatusInternalServerError)
532 return
533 }
giof6ad2982024-08-23 17:42:49 +0400534 clusters, err := s.m.GetClusters()
535 if err != nil {
536 http.Error(w, err.Error(), http.StatusInternalServerError)
537 return
538 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400539 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400540 App: a,
gio778577f2024-04-29 09:44:38 +0400541 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400542 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400543 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400544 AvailableClusters: clusters,
545 Task: t.task,
gio1cd65152024-08-16 08:18:49 +0400546 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400547 }
gioaa0fcdb2024-06-10 22:19:25 +0400548 if err := s.tmpl.app.Execute(w, data); err != nil {
549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
551 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552}
giof6ad2982024-08-23 17:42:49 +0400553
554type taskStatusData struct {
555 CurrentPage string
556 Task tasks.Task
557}
558
559func (s *AppManagerServer) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
560 s.l.Lock()
561 defer s.l.Unlock()
562 slug, ok := mux.Vars(r)["slug"]
563 if !ok {
564 http.Error(w, "empty slug", http.StatusBadRequest)
565 return
566 }
567 t, ok := s.tasks[slug]
568 if !ok {
569 http.Error(w, "task not found", http.StatusInternalServerError)
570
571 return
572 }
573 if ok && (t.task.Status() == tasks.StatusDone || t.task.Status() == tasks.StatusFailed) {
574 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
575 return
576 }
577 data := taskStatusData{
578 CurrentPage: "",
579 Task: t.task,
580 }
581 if err := s.tmpl.task.Execute(w, data); err != nil {
582 http.Error(w, err.Error(), http.StatusInternalServerError)
583 return
584 }
585}
586
587type clustersData struct {
588 CurrentPage string
589 Clusters []cluster.State
590}
591
592func (s *AppManagerServer) handleAllClusters(w http.ResponseWriter, r *http.Request) {
593 clusters, err := s.m.GetClusters()
594 if err != nil {
595 http.Error(w, err.Error(), http.StatusInternalServerError)
596 return
597 }
598 data := clustersData{
599 "clusters",
600 clusters,
601 }
602 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
603 http.Error(w, err.Error(), http.StatusInternalServerError)
604 return
605 }
606}
607
608type clusterData struct {
609 CurrentPage string
610 Cluster cluster.State
611}
612
613func (s *AppManagerServer) handleCluster(w http.ResponseWriter, r *http.Request) {
614 name, ok := mux.Vars(r)["name"]
615 if !ok {
616 http.Error(w, "empty name", http.StatusBadRequest)
617 return
618 }
619 m, err := s.getClusterManager(name)
620 if err != nil {
621 if errors.Is(err, installer.ErrorNotFound) {
622 http.Error(w, "not found", http.StatusNotFound)
623 } else {
624 http.Error(w, err.Error(), http.StatusInternalServerError)
625 }
626 return
627 }
628 data := clusterData{
629 "clusters",
630 m.State(),
631 }
632 if err := s.tmpl.cluster.Execute(w, data); err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
636}
637
638func (s *AppManagerServer) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
639 s.l.Lock()
640 defer s.l.Unlock()
641 cName, ok := mux.Vars(r)["cluster"]
642 if !ok {
643 http.Error(w, "empty name", http.StatusBadRequest)
644 return
645 }
646 if _, ok := s.tasks[cName]; ok {
647 http.Error(w, "cluster task in progress", http.StatusLocked)
648 return
649 }
650 sName, ok := mux.Vars(r)["server"]
651 if !ok {
652 http.Error(w, "empty name", http.StatusBadRequest)
653 return
654 }
655 m, err := s.getClusterManager(cName)
656 if err != nil {
657 if errors.Is(err, installer.ErrorNotFound) {
658 http.Error(w, "not found", http.StatusNotFound)
659 } else {
660 http.Error(w, err.Error(), http.StatusInternalServerError)
661 }
662 return
663 }
664 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
665 task.OnDone(func(err error) {
666 go func() {
667 time.Sleep(30 * time.Second)
668 s.l.Lock()
669 defer s.l.Unlock()
670 delete(s.tasks, cName)
671 }()
672 })
673 go task.Start()
674 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
675 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
676}
677
678func (s *AppManagerServer) getClusterManager(cName string) (cluster.Manager, error) {
679 clusters, err := s.m.GetClusters()
680 if err != nil {
681 return nil, err
682 }
683 var c *cluster.State
684 for _, i := range clusters {
685 if i.Name == cName {
686 c = &i
687 break
688 }
689 }
690 if c == nil {
691 return nil, installer.ErrorNotFound
692 }
693 return cluster.RestoreKubeManager(*c)
694}
695
696func (s *AppManagerServer) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
697 s.l.Lock()
698 defer s.l.Unlock()
699 cName, ok := mux.Vars(r)["cluster"]
700 if !ok {
701 http.Error(w, "empty name", http.StatusBadRequest)
702 return
703 }
704 if _, ok := s.tasks[cName]; ok {
705 http.Error(w, "cluster task in progress", http.StatusLocked)
706 return
707 }
708 m, err := s.getClusterManager(cName)
709 if err != nil {
710 if errors.Is(err, installer.ErrorNotFound) {
711 http.Error(w, "not found", http.StatusNotFound)
712 } else {
713 http.Error(w, err.Error(), http.StatusInternalServerError)
714 }
715 return
716 }
717 t := r.PostFormValue("type")
718 ip := net.ParseIP(r.PostFormValue("ip"))
719 if ip == nil {
720 http.Error(w, "invalid ip", http.StatusBadRequest)
721 return
722 }
723 port := 22
724 if p := r.PostFormValue("port"); p != "" {
725 port, err = strconv.Atoi(p)
726 if err != nil {
727 http.Error(w, err.Error(), http.StatusBadRequest)
728 return
729 }
730 }
731 server := cluster.Server{
732 IP: ip,
733 Port: port,
734 User: r.PostFormValue("user"),
735 Password: r.PostFormValue("password"),
736 }
737 var task tasks.Task
738 switch strings.ToLower(t) {
739 case "controller":
740 if len(m.State().Controllers) == 0 {
741 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
742 } else {
743 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
744 }
745 case "worker":
746 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
747 default:
748 http.Error(w, "invalid type", http.StatusBadRequest)
749 return
750 }
751 task.OnDone(func(err error) {
752 go func() {
753 time.Sleep(30 * time.Second)
754 s.l.Lock()
755 defer s.l.Unlock()
756 delete(s.tasks, cName)
757 }()
758 })
759 go task.Start()
760 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
761 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
762}
763
764func (s *AppManagerServer) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
765 cName := r.PostFormValue("name")
766 if cName == "" {
767 http.Error(w, "no name", http.StatusBadRequest)
768 return
769 }
770 st := cluster.State{Name: cName}
771 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
772 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
773 return "", err
774 }
775 return fmt.Sprintf("create cluster: %s", cName), nil
776 }); err != nil {
777 http.Error(w, err.Error(), http.StatusInternalServerError)
778 return
779 }
780 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
781}
782
783func (s *AppManagerServer) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
784 cName, ok := mux.Vars(r)["name"]
785 if !ok {
786 http.Error(w, "empty name", http.StatusBadRequest)
787 return
788 }
789 if _, ok := s.tasks[cName]; ok {
790 http.Error(w, "cluster task in progress", http.StatusLocked)
791 return
792 }
793 m, err := s.getClusterManager(cName)
794 if err != nil {
795 if errors.Is(err, installer.ErrorNotFound) {
796 http.Error(w, "not found", http.StatusNotFound)
797 } else {
798 http.Error(w, err.Error(), http.StatusInternalServerError)
799 }
800 return
801 }
802 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
803 task.OnDone(func(err error) {
804 go func() {
805 time.Sleep(30 * time.Second)
806 s.l.Lock()
807 defer s.l.Unlock()
808 delete(s.tasks, cName)
809 }()
810 })
811 go task.Start()
812 s.tasks[cName] = taskForward{task, fmt.Sprintf("/clusters/%s", cName)}
813 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
814}
815
816func (s *AppManagerServer) setupRemoteCluster() cluster.ClusterSetupFunc {
817 const vpnUser = "private-network-proxy"
818 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
819 hostname := fmt.Sprintf("cluster-%s", name)
820 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
821 app, err := installer.FindEnvApp(s.fr, "cluster-network")
822 if err != nil {
823 return installer.ReleaseResources{}, err
824 }
825 env, err := s.m.Config()
826 if err != nil {
827 return installer.ReleaseResources{}, err
828 }
829 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
830 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
831 namespace := fmt.Sprintf("%scluster-network-%s", env.NamespacePrefix, name)
832 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
833 "cluster": map[string]any{
834 "name": name,
835 "kubeconfig": kubeconfig,
836 "ingressClassName": ingressClassName,
837 },
838 // TODO(gio): remove hardcoded user
839 "vpnUser": vpnUser,
840 "vpnProxyHostname": hostname,
841 })
842 if err != nil {
843 return installer.ReleaseResources{}, err
844 }
845 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
846 go s.reconciler.Reconcile(ctx)
847 return rr, err
848 })
849 ch := make(chan error)
850 t.OnDone(func(err error) {
851 ch <- err
852 })
853 go t.Start()
854 err := <-ch
855 if err != nil {
856 return nil, err
857 }
858 for {
859 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
860 if err == nil {
861 return ip, nil
862 }
863 if errors.Is(err, installer.ErrorNotFound) {
864 time.Sleep(5 * time.Second)
865 }
866 }
867 }
868}