blob: 80f076ab5eabc6802ee7f5d5d281482cdcd85352 [file] [log] [blame]
gio59946282024-10-07 12:55:51 +04001package appmanager
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04002
3import (
giofc441e32024-11-11 16:26:14 +04004 "bytes"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +04005 "context"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04006 "embed"
7 "encoding/json"
giof6ad2982024-08-23 17:42:49 +04008 "errors"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04009 "fmt"
10 "html/template"
giof6ad2982024-08-23 17:42:49 +040011 "net"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040012 "net/http"
giof6ad2982024-08-23 17:42:49 +040013 "strconv"
14 "strings"
15 "sync"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040016 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040017
18 "github.com/Masterminds/sprig/v3"
gioaa0fcdb2024-06-10 22:19:25 +040019 "github.com/gorilla/mux"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040020
21 "github.com/giolekva/pcloud/core/installer"
giof6ad2982024-08-23 17:42:49 +040022 "github.com/giolekva/pcloud/core/installer/cluster"
gio59946282024-10-07 12:55:51 +040023 "github.com/giolekva/pcloud/core/installer/server"
giof6ad2982024-08-23 17:42:49 +040024 "github.com/giolekva/pcloud/core/installer/soft"
giof8acc612025-04-26 08:20:55 +040025 "github.com/giolekva/pcloud/core/installer/status"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040026 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040027)
28
gio59946282024-10-07 12:55:51 +040029//go:embed templates/*
30var templates embed.FS
31
32//go:embed static/*
33var staticAssets embed.FS
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040034
giof6ad2982024-08-23 17:42:49 +040035type taskForward struct {
36 task tasks.Task
37 redirectTo string
gio8c876172024-10-05 12:25:13 +040038 id int
giof6ad2982024-08-23 17:42:49 +040039}
40
gio59946282024-10-07 12:55:51 +040041type Server struct {
giof8acc612025-04-26 08:20:55 +040042 l sync.Locker
43 port int
44 ssClient soft.Client
45 repo soft.RepoIO
46 m *installer.AppManager
47 r installer.AppRepository
48 fr installer.AppRepository
49 reconciler *tasks.FixedReconciler
50 h status.ResourceMonitor
51 im *status.InstanceMonitor
52 cnc installer.ClusterNetworkConfigurator
53 vpnAPIClient installer.VPNAPIClient
54 tasks map[string]*taskForward
55 tmpl tmplts
56 idToResources map[string]map[string][]status.Resource
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040057}
58
59type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040060 index *template.Template
61 app *template.Template
62 allClusters *template.Template
63 cluster *template.Template
64 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040065}
66
gio59946282024-10-07 12:55:51 +040067func parseTemplates(fs embed.FS) (tmplts, error) {
68 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "templates/base.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040069 if err != nil {
70 return tmplts{}, err
71 }
72 parse := func(path string) (*template.Template, error) {
73 if b, err := base.Clone(); err != nil {
74 return nil, err
75 } else {
76 return b.ParseFS(fs, path)
77 }
78 }
gio59946282024-10-07 12:55:51 +040079 index, err := parse("templates/index.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040080 if err != nil {
81 return tmplts{}, err
82 }
gio59946282024-10-07 12:55:51 +040083 app, err := parse("templates/app.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040084 if err != nil {
85 return tmplts{}, err
86 }
gio59946282024-10-07 12:55:51 +040087 allClusters, err := parse("templates/all-clusters.html")
giof6ad2982024-08-23 17:42:49 +040088 if err != nil {
89 return tmplts{}, err
90 }
gio59946282024-10-07 12:55:51 +040091 cluster, err := parse("templates/cluster.html")
giof6ad2982024-08-23 17:42:49 +040092 if err != nil {
93 return tmplts{}, err
94 }
gio59946282024-10-07 12:55:51 +040095 task, err := parse("templates/task.html")
giof6ad2982024-08-23 17:42:49 +040096 if err != nil {
97 return tmplts{}, err
98 }
99 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400100}
101
gio59946282024-10-07 12:55:51 +0400102func NewServer(
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400103 port int,
gio721c0042025-04-03 11:56:36 +0400104 ssClient soft.Client,
giof6ad2982024-08-23 17:42:49 +0400105 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400106 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400107 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400108 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400109 reconciler *tasks.FixedReconciler,
giof8acc612025-04-26 08:20:55 +0400110 h status.ResourceMonitor,
111 im *status.InstanceMonitor,
giof6ad2982024-08-23 17:42:49 +0400112 cnc installer.ClusterNetworkConfigurator,
113 vpnAPIClient installer.VPNAPIClient,
gio59946282024-10-07 12:55:51 +0400114) (*Server, error) {
115 tmpl, err := parseTemplates(templates)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400116 if err != nil {
117 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400118 }
gio59946282024-10-07 12:55:51 +0400119 return &Server{
giof8acc612025-04-26 08:20:55 +0400120 l: &sync.Mutex{},
121 port: port,
122 ssClient: ssClient,
123 repo: repo,
124 m: m,
125 r: r,
126 fr: fr,
127 reconciler: reconciler,
128 h: h,
129 im: im,
130 cnc: cnc,
131 vpnAPIClient: vpnAPIClient,
132 tasks: make(map[string]*taskForward),
133 tmpl: tmpl,
134 idToResources: make(map[string]map[string][]status.Resource),
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400135 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400136}
137
gio59946282024-10-07 12:55:51 +0400138func (s *Server) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400139 r := mux.NewRouter()
gio59946282024-10-07 12:55:51 +0400140 r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
giocb34ad22024-07-11 08:01:13 +0400141 r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
giof15b9da2024-09-19 06:59:16 +0400142 r.HandleFunc("/api/clusters", s.handleClusters).Methods(http.MethodGet)
143 r.HandleFunc("/api/proxy/add", s.handleProxyAdd).Methods(http.MethodPost)
144 r.HandleFunc("/api/proxy/remove", s.handleProxyRemove).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400145 r.HandleFunc("/api/app-repo", s.handleAppRepo)
146 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
147 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
148 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
149 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
150 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
giof8acc612025-04-26 08:20:55 +0400151 r.HandleFunc("/api/instance/{instanceId}/status", s.handleInstanceStatusAPI).Methods(http.MethodGet)
gio63a1a822025-04-23 12:59:40 +0400152 r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
giofc441e32024-11-11 16:26:14 +0400153 r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400154 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
155 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
156 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400157 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400158 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
159 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
160 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400161 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
162 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400163 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400164 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
165 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400166 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400167 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400168}
169
giofc441e32024-11-11 16:26:14 +0400170type dodoAppInstallReq struct {
gio74e73e92025-04-20 11:57:44 +0400171 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400172}
173
gio218e8132025-04-22 17:11:58 +0000174type dodoAppInstallResp struct {
175 Id string `json:"id"`
176 DeployKey string `json:"deployKey"`
177}
178
179type dodoAppRendered struct {
180 Input struct {
181 Key struct {
182 Public string `json:"public"`
183 } `json:"key"`
184 } `json:"input"`
185}
186
giofc441e32024-11-11 16:26:14 +0400187func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400188 s.l.Lock()
189 defer s.l.Unlock()
giofc441e32024-11-11 16:26:14 +0400190 var req dodoAppInstallReq
191 // TODO(gio): validate that no internal fields are overridden by request
192 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
193 http.Error(w, err.Error(), http.StatusBadRequest)
194 return
195 }
196 clusters, err := s.m.GetClusters()
197 if err != nil {
198 http.Error(w, err.Error(), http.StatusInternalServerError)
199 return
200 }
201 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
202 var cfg bytes.Buffer
203 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
204 http.Error(w, err.Error(), http.StatusInternalServerError)
205 return
206 }
207 app, err := installer.NewDodoApp(cfg.Bytes())
208 if err != nil {
209 http.Error(w, err.Error(), http.StatusBadRequest)
210 return
211 }
gio218e8132025-04-22 17:11:58 +0000212 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400213 http.Error(w, err.Error(), http.StatusInternalServerError)
214 return
gioa421b062025-04-21 09:45:04 +0400215 } else {
giof8acc612025-04-26 08:20:55 +0400216 var toMonitor []status.Resource
217 s.idToResources[instanceId] = map[string][]status.Resource{}
218 for _, r := range rr.Helm {
219 resource := status.Resource{
220 Type: status.ResourceHelmRelease,
221 ResourceRef: status.ResourceRef{
222 Name: r.Name,
223 Namespace: r.Namespace,
224 },
225 }
226 toMonitor = append(toMonitor, resource)
227 if tmp, ok := s.idToResources[instanceId][r.Id]; ok {
228 s.idToResources[instanceId][r.Id] = append(tmp, resource)
229 } else {
230 s.idToResources[instanceId][r.Id] = []status.Resource{resource}
231 }
232 }
233 s.im.Monitor(instanceId, toMonitor)
gio218e8132025-04-22 17:11:58 +0000234 var cfg dodoAppRendered
235 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
236 http.Error(w, err.Error(), http.StatusInternalServerError)
237 }
238 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
239 Id: instanceId,
240 DeployKey: cfg.Input.Key.Public,
241 }); err != nil {
242 http.Error(w, err.Error(), http.StatusInternalServerError)
243 }
giofc441e32024-11-11 16:26:14 +0400244 }
245}
246
gio63a1a822025-04-23 12:59:40 +0400247func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400248 s.l.Lock()
249 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400250 instanceId, ok := mux.Vars(r)["instanceId"]
251 if !ok {
252 http.Error(w, "missing instance id", http.StatusBadRequest)
253 }
gio268787a2025-04-24 21:18:06 +0400254 if _, ok := s.tasks[instanceId]; ok {
255 http.Error(w, "task in progress", http.StatusTooEarly)
256 return
257 }
gio63a1a822025-04-23 12:59:40 +0400258 var req dodoAppInstallReq
259 // TODO(gio): validate that no internal fields are overridden by request
260 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
261 http.Error(w, err.Error(), http.StatusBadRequest)
262 return
263 }
264 clusters, err := s.m.GetClusters()
265 if err != nil {
266 http.Error(w, err.Error(), http.StatusInternalServerError)
267 return
268 }
269 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
270 var cfg bytes.Buffer
271 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
274 }
275 overrides := installer.CueAppData{
276 "app.cue": cfg.Bytes(),
277 }
gio268787a2025-04-24 21:18:06 +0400278 rr, err := s.m.Update(instanceId, nil, overrides)
279 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400280 http.Error(w, err.Error(), http.StatusInternalServerError)
281 }
giof8acc612025-04-26 08:20:55 +0400282 var toMonitor []status.Resource
283 s.idToResources[instanceId] = map[string][]status.Resource{}
284 for _, r := range rr.Helm {
285 resource := status.Resource{
286 Type: status.ResourceHelmRelease,
287 ResourceRef: status.ResourceRef{
288 Name: r.Name,
289 Namespace: r.Namespace,
290 },
291 }
292 toMonitor = append(toMonitor, resource)
293 if tmp, ok := s.idToResources[instanceId][r.Id]; ok {
294 s.idToResources[instanceId][r.Id] = append(tmp, resource)
295 } else {
296 s.idToResources[instanceId][r.Id] = []status.Resource{resource}
297 }
298 }
299 s.im.Monitor(instanceId, toMonitor)
gio268787a2025-04-24 21:18:06 +0400300 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
301 if err == nil {
302 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
303 go s.reconciler.Reconcile(ctx)
304 }
305 return rr, err
306 })
307 if _, ok := s.tasks[instanceId]; ok {
308 panic("MUST NOT REACH!")
309 }
310 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
311 t.OnDone(s.cleanTask(instanceId, 0))
312 go t.Start()
gio63a1a822025-04-23 12:59:40 +0400313}
314
gio59946282024-10-07 12:55:51 +0400315func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400316 env, err := s.m.Config()
317 if err != nil {
318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
320 }
321 networks, err := s.m.CreateNetworks(env)
322 if err != nil {
323 http.Error(w, err.Error(), http.StatusInternalServerError)
324 return
325 }
326 if err := json.NewEncoder(w).Encode(networks); err != nil {
327 http.Error(w, err.Error(), http.StatusInternalServerError)
328 return
329 }
330}
331
gio59946282024-10-07 12:55:51 +0400332func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400333 clusters, err := s.m.GetClusters()
334 if err != nil {
335 http.Error(w, err.Error(), http.StatusInternalServerError)
336 return
337 }
338 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
339 http.Error(w, err.Error(), http.StatusInternalServerError)
340 return
341 }
342}
343
344type proxyPair struct {
345 From string `json:"from"`
346 To string `json:"to"`
347}
348
gio59946282024-10-07 12:55:51 +0400349func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400350 var req proxyPair
351 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
352 http.Error(w, err.Error(), http.StatusBadRequest)
353 return
354 }
gio721c0042025-04-03 11:56:36 +0400355 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400356 http.Error(w, err.Error(), http.StatusInternalServerError)
357 return
358 }
359}
360
gio59946282024-10-07 12:55:51 +0400361func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400362 var req proxyPair
363 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
364 http.Error(w, err.Error(), http.StatusBadRequest)
365 return
366 }
gio721c0042025-04-03 11:56:36 +0400367 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400368 http.Error(w, err.Error(), http.StatusInternalServerError)
369 return
370 }
371}
372
373type app struct {
374 Name string `json:"name"`
375 Icon template.HTML `json:"icon"`
376 ShortDescription string `json:"shortDescription"`
377 Slug string `json:"slug"`
378 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
379}
380
gio59946282024-10-07 12:55:51 +0400381func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400382 all, err := s.r.GetAll()
383 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400384 http.Error(w, err.Error(), http.StatusInternalServerError)
385 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400386 }
387 resp := make([]app, len(all))
388 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400389 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400390 }
gioaa0fcdb2024-06-10 22:19:25 +0400391 w.Header().Set("Content-Type", "application/json")
392 if err := json.NewEncoder(w).Encode(resp); err != nil {
393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
395 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400396}
397
gio59946282024-10-07 12:55:51 +0400398func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400399 slug, ok := mux.Vars(r)["slug"]
400 if !ok {
401 http.Error(w, "empty slug", http.StatusBadRequest)
402 return
403 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400404 a, err := s.r.Find(slug)
405 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400408 }
gio7fbd4ad2024-08-27 10:06:39 +0400409 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400410 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400413 }
gioaa0fcdb2024-06-10 22:19:25 +0400414 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
415 w.Header().Set("Content-Type", "application/json")
416 if err := json.NewEncoder(w).Encode(resp); err != nil {
417 http.Error(w, err.Error(), http.StatusInternalServerError)
418 return
419 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400420}
421
gio59946282024-10-07 12:55:51 +0400422func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400423 slug, ok := mux.Vars(r)["slug"]
424 if !ok {
425 http.Error(w, "empty slug", http.StatusBadRequest)
426 return
427 }
gio7fbd4ad2024-08-27 10:06:39 +0400428 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400429 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400430 http.Error(w, err.Error(), http.StatusInternalServerError)
431 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400432 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400433 a, err := s.r.Find(instance.AppId)
434 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400435 http.Error(w, err.Error(), http.StatusInternalServerError)
436 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400437 }
gioaa0fcdb2024-06-10 22:19:25 +0400438 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
439 w.Header().Set("Content-Type", "application/json")
440 if err := json.NewEncoder(w).Encode(resp); err != nil {
441 http.Error(w, err.Error(), http.StatusInternalServerError)
442 return
443 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400444}
445
gio218e8132025-04-22 17:11:58 +0000446func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400447 env, err := s.m.Config()
448 if err != nil {
gio218e8132025-04-22 17:11:58 +0000449 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400450 }
451 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
452 suffix, err := suffixGen.Generate()
453 if err != nil {
gio218e8132025-04-22 17:11:58 +0000454 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400455 }
456 instanceId := app.Slug() + suffix
457 appDir := fmt.Sprintf("/apps/%s", instanceId)
458 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000459 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400460 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400461 if err == nil {
462 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
463 go s.reconciler.Reconcile(ctx)
464 }
465 return rr, err
466 })
467 if _, ok := s.tasks[instanceId]; ok {
468 panic("MUST NOT REACH!")
469 }
470 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
471 t.OnDone(s.cleanTask(instanceId, 0))
472 go t.Start()
gio218e8132025-04-22 17:11:58 +0000473 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400474}
475
gio59946282024-10-07 12:55:51 +0400476func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400477 s.l.Lock()
478 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400479 slug, ok := mux.Vars(r)["slug"]
480 if !ok {
481 http.Error(w, "empty slug", http.StatusBadRequest)
482 return
483 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400484 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400485 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400486 http.Error(w, err.Error(), http.StatusInternalServerError)
487 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400488 }
gioa421b062025-04-21 09:45:04 +0400489 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400490 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400491 http.Error(w, err.Error(), http.StatusInternalServerError)
492 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400493 }
gio218e8132025-04-22 17:11:58 +0000494 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400495 http.Error(w, err.Error(), http.StatusInternalServerError)
496 return
gioa421b062025-04-21 09:45:04 +0400497 } else {
498 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400499 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400500}
501
gio59946282024-10-07 12:55:51 +0400502func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400503 s.l.Lock()
504 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400505 slug, ok := mux.Vars(r)["slug"]
506 if !ok {
507 http.Error(w, "empty slug", http.StatusBadRequest)
508 return
509 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400510 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400511 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400514 }
gio8c876172024-10-05 12:25:13 +0400515 tid := 0
516 if t, ok := s.tasks[slug]; ok {
517 if t.task != nil {
518 http.Error(w, "Update already in progress", http.StatusBadRequest)
519 return
520 }
521 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400522 }
gio63a1a822025-04-23 12:59:40 +0400523 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400524 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400525 http.Error(w, err.Error(), http.StatusInternalServerError)
526 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400527 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400528 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
529 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400530 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400531 t.OnDone(s.cleanTask(slug, tid))
532 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400533 go t.Start()
gio268787a2025-04-24 21:18:06 +0400534 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400535 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400536 http.Error(w, err.Error(), http.StatusInternalServerError)
537 return
538 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400539}
540
gio59946282024-10-07 12:55:51 +0400541func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400542 slug, ok := mux.Vars(r)["slug"]
543 if !ok {
544 http.Error(w, "empty slug", http.StatusBadRequest)
545 return
546 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400547 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400550 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400551 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
552 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400553 if _, err := fmt.Fprint(w, "/"); err != nil {
554 http.Error(w, err.Error(), http.StatusInternalServerError)
555 return
556 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400557}
558
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400559type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400560 Apps []app
561 CurrentPage string
562 SearchTarget string
563 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400564}
565
gio59946282024-10-07 12:55:51 +0400566func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400567 pageType := mux.Vars(r)["pageType"]
568 if pageType == "" {
569 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400570 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400571 searchQuery := r.FormValue("query")
572 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400573 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400574 http.Error(w, err.Error(), http.StatusInternalServerError)
575 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400576 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400577 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400578 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400579 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400580 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400581 http.Error(w, err.Error(), http.StatusInternalServerError)
582 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400583 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400584 switch pageType {
585 case "installed":
586 if len(instances) != 0 {
587 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
588 }
589 case "not-installed":
590 if len(instances) == 0 {
591 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
592 }
593 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400594 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
595 }
596 }
597 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400598 Apps: resp,
599 CurrentPage: pageType,
600 SearchTarget: pageType,
601 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400602 }
gioaa0fcdb2024-06-10 22:19:25 +0400603 if err := s.tmpl.index.Execute(w, data); err != nil {
604 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400605 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400606}
607
608type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400609 App installer.EnvApp
610 Instance *installer.AppInstanceConfig
611 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400612 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400613 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400614 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400615}
616
gio59946282024-10-07 12:55:51 +0400617func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400618 global, err := s.m.Config()
619 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400622 }
gioaa0fcdb2024-06-10 22:19:25 +0400623 slug, ok := mux.Vars(r)["slug"]
624 if !ok {
625 http.Error(w, "empty slug", http.StatusBadRequest)
626 return
627 }
gio3cdee592024-04-17 10:15:56 +0400628 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400629 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400630 http.Error(w, err.Error(), http.StatusInternalServerError)
631 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400632 }
gio7fbd4ad2024-08-27 10:06:39 +0400633 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400634 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400637 }
giocb34ad22024-07-11 08:01:13 +0400638 networks, err := s.m.CreateNetworks(global)
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
giof6ad2982024-08-23 17:42:49 +0400643 clusters, err := s.m.GetClusters()
644 if err != nil {
645 http.Error(w, err.Error(), http.StatusInternalServerError)
646 return
647 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400648 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400649 App: a,
650 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400651 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400652 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400653 CurrentPage: a.Name(),
654 }
gioaa0fcdb2024-06-10 22:19:25 +0400655 if err := s.tmpl.app.Execute(w, data); err != nil {
656 http.Error(w, err.Error(), http.StatusInternalServerError)
657 return
658 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400659}
660
gio59946282024-10-07 12:55:51 +0400661func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400662 s.l.Lock()
663 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400664 global, err := s.m.Config()
665 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400666 http.Error(w, err.Error(), http.StatusInternalServerError)
667 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400668 }
gioaa0fcdb2024-06-10 22:19:25 +0400669 slug, ok := mux.Vars(r)["slug"]
670 if !ok {
671 http.Error(w, "empty slug", http.StatusBadRequest)
672 return
673 }
gio8c876172024-10-05 12:25:13 +0400674 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400675 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
676 return
677 }
gio8c876172024-10-05 12:25:13 +0400678 instance, err := s.m.GetInstance(slug)
679 if err != nil {
680 http.Error(w, err.Error(), http.StatusInternalServerError)
681 return
682 }
gio63a1a822025-04-23 12:59:40 +0400683 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400684 if err != nil {
685 http.Error(w, err.Error(), http.StatusInternalServerError)
686 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400687 }
gio7fbd4ad2024-08-27 10:06:39 +0400688 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400689 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400690 http.Error(w, err.Error(), http.StatusInternalServerError)
691 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400692 }
giocb34ad22024-07-11 08:01:13 +0400693 networks, err := s.m.CreateNetworks(global)
694 if err != nil {
695 http.Error(w, err.Error(), http.StatusInternalServerError)
696 return
697 }
giof6ad2982024-08-23 17:42:49 +0400698 clusters, err := s.m.GetClusters()
699 if err != nil {
700 http.Error(w, err.Error(), http.StatusInternalServerError)
701 return
702 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400703 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400704 App: a,
gio778577f2024-04-29 09:44:38 +0400705 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400706 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400707 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400708 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400709 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400710 }
gioaa0fcdb2024-06-10 22:19:25 +0400711 if err := s.tmpl.app.Execute(w, data); err != nil {
712 http.Error(w, err.Error(), http.StatusInternalServerError)
713 return
714 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400715}
giof6ad2982024-08-23 17:42:49 +0400716
717type taskStatusData struct {
718 CurrentPage string
719 Task tasks.Task
720}
721
gio59946282024-10-07 12:55:51 +0400722func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400723 s.l.Lock()
724 defer s.l.Unlock()
725 slug, ok := mux.Vars(r)["slug"]
726 if !ok {
727 http.Error(w, "empty slug", http.StatusBadRequest)
728 return
729 }
730 t, ok := s.tasks[slug]
731 if !ok {
732 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400733 return
734 }
gio8c876172024-10-05 12:25:13 +0400735 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400736 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
737 return
738 }
739 data := taskStatusData{
740 CurrentPage: "",
741 Task: t.task,
742 }
743 if err := s.tmpl.task.Execute(w, data); err != nil {
744 http.Error(w, err.Error(), http.StatusInternalServerError)
745 return
746 }
747}
748
gio268787a2025-04-24 21:18:06 +0400749type resourceStatus struct {
750 Type string `json:"type"`
751 Name string `json:"name"`
752 Status string `json:"status"`
753}
754
755func extractResources(t tasks.Task) []resourceStatus {
756 var ret []resourceStatus
757 if t.Resource() != nil {
758 ret = append(ret, resourceStatus{
759 Type: t.Resource().Type,
760 Name: t.Resource().Name,
761 Status: tasks.StatusString(t.Status()),
762 })
763 }
764 for _, st := range t.Subtasks() {
765 ret = append(ret, extractResources(st)...)
766 }
767 return ret
768}
769
giof8acc612025-04-26 08:20:55 +0400770type IdName struct {
771 Id string
772 Name string
773}
774
775type IdNameMap map[string]IdName
776
777type resourceOuts struct {
778 Outs map[string]struct {
779 PostgreSQL IdNameMap `json:"postgresql"`
780 MongoDB IdNameMap `json:"mongodb"`
781 Volume IdNameMap `json:"volume"`
782 Ingress IdNameMap `json:"ingress"`
783 } `json:"outs"`
784}
785
786type DodoResource struct {
787 Type string
788 Name string
789}
790
791type DodoResourceStatus struct {
792 Type string `json:"type"`
793 Name string `json:"name"`
794 Status string `json:"status"`
795}
796
797func orginize(raw []byte) (map[string]DodoResource, error) {
798 var outs resourceOuts
799 if err := json.NewDecoder(bytes.NewReader(raw)).Decode(&outs); err != nil {
800 return nil, err
801 }
802 ret := map[string]DodoResource{}
803 for _, out := range outs.Outs {
804 for _, r := range out.PostgreSQL {
805 ret[r.Id] = DodoResource{
806 Type: "postgresql",
807 Name: r.Name,
808 }
809 }
810 for _, r := range out.MongoDB {
811 ret[r.Id] = DodoResource{
812 Type: "mongodb",
813 Name: r.Name,
814 }
815 }
816 for _, r := range out.Volume {
817 ret[r.Id] = DodoResource{
818 Type: "volume",
819 Name: r.Name,
820 }
821 }
822 for _, r := range out.Ingress {
823 ret[r.Id] = DodoResource{
824 Type: "ingress",
825 Name: r.Name,
826 }
827 }
828 }
829 return ret, nil
830}
831
832func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400833 s.l.Lock()
834 defer s.l.Unlock()
835 instanceId, ok := mux.Vars(r)["instanceId"]
836 if !ok {
837 http.Error(w, "empty slug", http.StatusBadRequest)
838 return
839 }
giof8acc612025-04-26 08:20:55 +0400840 statuses, err := s.im.Get(instanceId)
841 if err != nil {
842 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400843 return
844 }
giof8acc612025-04-26 08:20:55 +0400845 idStatus := map[string]status.Status{}
846 for id, resources := range s.idToResources[instanceId] {
847 st := status.StatusNoStatus
848 for _, resource := range resources {
849 if st < statuses[resource] {
850 st = statuses[resource]
851 }
852 }
853 idStatus[id] = st
854 }
855 s.repo.Pull()
856 rendered, err := s.m.AppRendered(instanceId)
857 if err != nil {
858 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400859 return
860 }
giof8acc612025-04-26 08:20:55 +0400861 idToResource, err := orginize(rendered)
862 if err != nil {
863 http.Error(w, err.Error(), http.StatusInternalServerError)
864 return
865 }
866 resources := []DodoResourceStatus{}
867 for id, st := range idStatus {
868 resources = append(resources, DodoResourceStatus{
869 Type: idToResource[id].Type,
870 Name: idToResource[id].Name,
871 Status: status.StatusString(st),
872 })
873 }
gio268787a2025-04-24 21:18:06 +0400874 json.NewEncoder(w).Encode(resources)
875}
876
giof6ad2982024-08-23 17:42:49 +0400877type clustersData struct {
878 CurrentPage string
879 Clusters []cluster.State
880}
881
gio59946282024-10-07 12:55:51 +0400882func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400883 clusters, err := s.m.GetClusters()
884 if err != nil {
885 http.Error(w, err.Error(), http.StatusInternalServerError)
886 return
887 }
888 data := clustersData{
889 "clusters",
890 clusters,
891 }
892 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
893 http.Error(w, err.Error(), http.StatusInternalServerError)
894 return
895 }
896}
897
898type clusterData struct {
899 CurrentPage string
900 Cluster cluster.State
901}
902
gio59946282024-10-07 12:55:51 +0400903func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400904 name, ok := mux.Vars(r)["name"]
905 if !ok {
906 http.Error(w, "empty name", http.StatusBadRequest)
907 return
908 }
909 m, err := s.getClusterManager(name)
910 if err != nil {
911 if errors.Is(err, installer.ErrorNotFound) {
912 http.Error(w, "not found", http.StatusNotFound)
913 } else {
914 http.Error(w, err.Error(), http.StatusInternalServerError)
915 }
916 return
917 }
918 data := clusterData{
919 "clusters",
920 m.State(),
921 }
922 if err := s.tmpl.cluster.Execute(w, data); err != nil {
923 http.Error(w, err.Error(), http.StatusInternalServerError)
924 return
925 }
926}
927
gio59946282024-10-07 12:55:51 +0400928func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400929 cName, ok := mux.Vars(r)["name"]
930 if !ok {
931 http.Error(w, "empty name", http.StatusBadRequest)
932 return
933 }
gio8c876172024-10-05 12:25:13 +0400934 tid := 0
935 if t, ok := s.tasks[cName]; ok {
936 if t.task != nil {
937 http.Error(w, "cluster task in progress", http.StatusLocked)
938 return
939 }
940 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400941 }
942 m, err := s.getClusterManager(cName)
943 if err != nil {
944 if errors.Is(err, installer.ErrorNotFound) {
945 http.Error(w, "not found", http.StatusNotFound)
946 } else {
947 http.Error(w, err.Error(), http.StatusInternalServerError)
948 }
949 return
950 }
951 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400952 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400953 go task.Start()
gio8c876172024-10-05 12:25:13 +0400954 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400955 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
956}
957
gio59946282024-10-07 12:55:51 +0400958func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400959 s.l.Lock()
960 defer s.l.Unlock()
961 cName, ok := mux.Vars(r)["cluster"]
962 if !ok {
963 http.Error(w, "empty name", http.StatusBadRequest)
964 return
965 }
gio8c876172024-10-05 12:25:13 +0400966 tid := 0
967 if t, ok := s.tasks[cName]; ok {
968 if t.task != nil {
969 http.Error(w, "cluster task in progress", http.StatusLocked)
970 return
971 }
972 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400973 }
974 sName, ok := mux.Vars(r)["server"]
975 if !ok {
976 http.Error(w, "empty name", http.StatusBadRequest)
977 return
978 }
979 m, err := s.getClusterManager(cName)
980 if err != nil {
981 if errors.Is(err, installer.ErrorNotFound) {
982 http.Error(w, "not found", http.StatusNotFound)
983 } else {
984 http.Error(w, err.Error(), http.StatusInternalServerError)
985 }
986 return
987 }
988 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400989 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400990 go task.Start()
gio8c876172024-10-05 12:25:13 +0400991 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400992 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
993}
994
gio59946282024-10-07 12:55:51 +0400995func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400996 clusters, err := s.m.GetClusters()
997 if err != nil {
998 return nil, err
999 }
1000 var c *cluster.State
1001 for _, i := range clusters {
1002 if i.Name == cName {
1003 c = &i
1004 break
1005 }
1006 }
1007 if c == nil {
1008 return nil, installer.ErrorNotFound
1009 }
1010 return cluster.RestoreKubeManager(*c)
1011}
1012
gio59946282024-10-07 12:55:51 +04001013func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001014 s.l.Lock()
1015 defer s.l.Unlock()
1016 cName, ok := mux.Vars(r)["cluster"]
1017 if !ok {
1018 http.Error(w, "empty name", http.StatusBadRequest)
1019 return
1020 }
gio8c876172024-10-05 12:25:13 +04001021 tid := 0
1022 if t, ok := s.tasks[cName]; ok {
1023 if t.task != nil {
1024 http.Error(w, "cluster task in progress", http.StatusLocked)
1025 return
1026 }
1027 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001028 }
1029 m, err := s.getClusterManager(cName)
1030 if err != nil {
1031 if errors.Is(err, installer.ErrorNotFound) {
1032 http.Error(w, "not found", http.StatusNotFound)
1033 } else {
1034 http.Error(w, err.Error(), http.StatusInternalServerError)
1035 }
1036 return
1037 }
1038 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +04001039 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +04001040 if ip == nil {
1041 http.Error(w, "invalid ip", http.StatusBadRequest)
1042 return
1043 }
1044 port := 22
1045 if p := r.PostFormValue("port"); p != "" {
1046 port, err = strconv.Atoi(p)
1047 if err != nil {
1048 http.Error(w, err.Error(), http.StatusBadRequest)
1049 return
1050 }
1051 }
1052 server := cluster.Server{
1053 IP: ip,
1054 Port: port,
1055 User: r.PostFormValue("user"),
1056 Password: r.PostFormValue("password"),
1057 }
1058 var task tasks.Task
1059 switch strings.ToLower(t) {
1060 case "controller":
1061 if len(m.State().Controllers) == 0 {
1062 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
1063 } else {
1064 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
1065 }
1066 case "worker":
1067 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
1068 default:
1069 http.Error(w, "invalid type", http.StatusBadRequest)
1070 return
1071 }
gio8c876172024-10-05 12:25:13 +04001072 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001073 go task.Start()
gio8c876172024-10-05 12:25:13 +04001074 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001075 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1076}
1077
gio59946282024-10-07 12:55:51 +04001078func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001079 cName := r.PostFormValue("name")
1080 if cName == "" {
1081 http.Error(w, "no name", http.StatusBadRequest)
1082 return
1083 }
1084 st := cluster.State{Name: cName}
1085 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
1086 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
1087 return "", err
1088 }
1089 return fmt.Sprintf("create cluster: %s", cName), nil
1090 }); err != nil {
1091 http.Error(w, err.Error(), http.StatusInternalServerError)
1092 return
1093 }
1094 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
1095}
1096
gio59946282024-10-07 12:55:51 +04001097func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001098 cName, ok := mux.Vars(r)["name"]
1099 if !ok {
1100 http.Error(w, "empty name", http.StatusBadRequest)
1101 return
1102 }
gio8c876172024-10-05 12:25:13 +04001103 tid := 0
1104 if t, ok := s.tasks[cName]; ok {
1105 if t.task != nil {
1106 http.Error(w, "cluster task in progress", http.StatusLocked)
1107 return
1108 }
1109 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001110 }
1111 m, err := s.getClusterManager(cName)
1112 if err != nil {
1113 if errors.Is(err, installer.ErrorNotFound) {
1114 http.Error(w, "not found", http.StatusNotFound)
1115 } else {
1116 http.Error(w, err.Error(), http.StatusInternalServerError)
1117 }
1118 return
1119 }
1120 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +04001121 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001122 go task.Start()
gio8c876172024-10-05 12:25:13 +04001123 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001124 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1125}
1126
gio59946282024-10-07 12:55:51 +04001127func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001128 const vpnUser = "private-network-proxy"
1129 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1130 hostname := fmt.Sprintf("cluster-%s", name)
1131 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1132 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1133 if err != nil {
1134 return installer.ReleaseResources{}, err
1135 }
1136 env, err := s.m.Config()
1137 if err != nil {
1138 return installer.ReleaseResources{}, err
1139 }
gio721c0042025-04-03 11:56:36 +04001140 keys, err := installer.NewSSHKeyPair("port-allocator")
1141 if err != nil {
1142 return installer.ReleaseResources{}, err
1143 }
1144 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1145 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1146 return installer.ReleaseResources{}, err
1147 }
1148 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1149 return installer.ReleaseResources{}, err
1150 }
giof6ad2982024-08-23 17:42:49 +04001151 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1152 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001153 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001154 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1155 "cluster": map[string]any{
1156 "name": name,
1157 "kubeconfig": kubeconfig,
1158 "ingressClassName": ingressClassName,
1159 },
1160 // TODO(gio): remove hardcoded user
1161 "vpnUser": vpnUser,
1162 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001163 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001164 })
1165 if err != nil {
1166 return installer.ReleaseResources{}, err
1167 }
1168 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1169 go s.reconciler.Reconcile(ctx)
1170 return rr, err
1171 })
1172 ch := make(chan error)
1173 t.OnDone(func(err error) {
1174 ch <- err
1175 })
1176 go t.Start()
1177 err := <-ch
1178 if err != nil {
1179 return nil, err
1180 }
1181 for {
1182 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1183 if err == nil {
1184 return ip, nil
1185 }
1186 if errors.Is(err, installer.ErrorNotFound) {
1187 time.Sleep(5 * time.Second)
1188 }
1189 }
1190 }
1191}
gio8f290322024-09-21 15:37:45 +04001192
gio59946282024-10-07 12:55:51 +04001193func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001194 return func(cm cluster.Manager) error {
1195 name := cm.State().Name
1196 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1197 app, err := installer.FindEnvApp(s.fr, "longhorn")
1198 if err != nil {
1199 return installer.ReleaseResources{}, err
1200 }
1201 env, err := s.m.Config()
1202 if err != nil {
1203 return installer.ReleaseResources{}, err
1204 }
1205 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1206 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1207 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1208 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1209 "cluster": name,
1210 })
1211 if err != nil {
1212 return installer.ReleaseResources{}, err
1213 }
1214 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1215 go s.reconciler.Reconcile(ctx)
1216 return rr, err
1217 })
1218 ch := make(chan error)
1219 t.OnDone(func(err error) {
1220 ch <- err
1221 })
1222 go t.Start()
1223 err := <-ch
1224 if err != nil {
1225 return err
1226 }
1227 cm.EnableStorage()
1228 return nil
1229 }
1230}
gio8c876172024-10-05 12:25:13 +04001231
gio59946282024-10-07 12:55:51 +04001232func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001233 return func(err error) {
1234 if err != nil {
1235 fmt.Printf("Task %s failed: %s", name, err.Error())
1236 }
1237 s.l.Lock()
1238 defer s.l.Unlock()
1239 s.tasks[name].task = nil
1240 go func() {
1241 time.Sleep(30 * time.Second)
1242 s.l.Lock()
1243 defer s.l.Unlock()
1244 if t, ok := s.tasks[name]; ok && t.id == id {
1245 delete(s.tasks, name)
1246 }
1247 }()
1248 }
1249}