blob: ffa710c3ca6c1fea9e10dea01f5d4f70bda7468e [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 {
gioda708652025-04-30 14:57:38 +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
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040056}
57
58type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040059 index *template.Template
60 app *template.Template
61 allClusters *template.Template
62 cluster *template.Template
63 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040064}
65
gio59946282024-10-07 12:55:51 +040066func parseTemplates(fs embed.FS) (tmplts, error) {
67 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "templates/base.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040068 if err != nil {
69 return tmplts{}, err
70 }
71 parse := func(path string) (*template.Template, error) {
72 if b, err := base.Clone(); err != nil {
73 return nil, err
74 } else {
75 return b.ParseFS(fs, path)
76 }
77 }
gio59946282024-10-07 12:55:51 +040078 index, err := parse("templates/index.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040079 if err != nil {
80 return tmplts{}, err
81 }
gio59946282024-10-07 12:55:51 +040082 app, err := parse("templates/app.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040083 if err != nil {
84 return tmplts{}, err
85 }
gio59946282024-10-07 12:55:51 +040086 allClusters, err := parse("templates/all-clusters.html")
giof6ad2982024-08-23 17:42:49 +040087 if err != nil {
88 return tmplts{}, err
89 }
gio59946282024-10-07 12:55:51 +040090 cluster, err := parse("templates/cluster.html")
giof6ad2982024-08-23 17:42:49 +040091 if err != nil {
92 return tmplts{}, err
93 }
gio59946282024-10-07 12:55:51 +040094 task, err := parse("templates/task.html")
giof6ad2982024-08-23 17:42:49 +040095 if err != nil {
96 return tmplts{}, err
97 }
98 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040099}
100
gio59946282024-10-07 12:55:51 +0400101func NewServer(
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400102 port int,
gio721c0042025-04-03 11:56:36 +0400103 ssClient soft.Client,
giof6ad2982024-08-23 17:42:49 +0400104 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400105 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400106 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400107 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400108 reconciler *tasks.FixedReconciler,
giof8acc612025-04-26 08:20:55 +0400109 h status.ResourceMonitor,
110 im *status.InstanceMonitor,
giof6ad2982024-08-23 17:42:49 +0400111 cnc installer.ClusterNetworkConfigurator,
112 vpnAPIClient installer.VPNAPIClient,
gio59946282024-10-07 12:55:51 +0400113) (*Server, error) {
114 tmpl, err := parseTemplates(templates)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400115 if err != nil {
116 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400117 }
gio59946282024-10-07 12:55:51 +0400118 return &Server{
gioda708652025-04-30 14:57:38 +0400119 l: &sync.Mutex{},
120 port: port,
121 ssClient: ssClient,
122 repo: repo,
123 m: m,
124 r: r,
125 fr: fr,
126 reconciler: reconciler,
127 h: h,
128 im: im,
129 cnc: cnc,
130 vpnAPIClient: vpnAPIClient,
131 tasks: make(map[string]*taskForward),
132 tmpl: tmpl,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400133 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400134}
135
gio59946282024-10-07 12:55:51 +0400136func (s *Server) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400137 r := mux.NewRouter()
gio59946282024-10-07 12:55:51 +0400138 r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
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)
giof8acc612025-04-26 08:20:55 +0400149 r.HandleFunc("/api/instance/{instanceId}/status", s.handleInstanceStatusAPI).Methods(http.MethodGet)
gio63a1a822025-04-23 12:59:40 +0400150 r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
giofc441e32024-11-11 16:26:14 +0400151 r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400152 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
153 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
154 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400155 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400156 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
157 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
158 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400159 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
160 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400161 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400162 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
163 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400164 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400165 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400166}
167
giofc441e32024-11-11 16:26:14 +0400168type dodoAppInstallReq struct {
gio74e73e92025-04-20 11:57:44 +0400169 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400170}
171
gio218e8132025-04-22 17:11:58 +0000172type dodoAppInstallResp struct {
gio6ce44812025-05-17 07:31:54 +0400173 Id string `json:"id"`
174 DeployKey string `json:"deployKey"`
175 Access []installer.Access `json:"access"`
gio218e8132025-04-22 17:11:58 +0000176}
177
178type dodoAppRendered struct {
179 Input struct {
180 Key struct {
181 Public string `json:"public"`
182 } `json:"key"`
183 } `json:"input"`
184}
185
giofc441e32024-11-11 16:26:14 +0400186func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400187 s.l.Lock()
188 defer s.l.Unlock()
giofc441e32024-11-11 16:26:14 +0400189 var req dodoAppInstallReq
190 // TODO(gio): validate that no internal fields are overridden by request
191 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
192 http.Error(w, err.Error(), http.StatusBadRequest)
193 return
194 }
195 clusters, err := s.m.GetClusters()
196 if err != nil {
197 http.Error(w, err.Error(), http.StatusInternalServerError)
198 return
199 }
200 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
201 var cfg bytes.Buffer
202 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
203 http.Error(w, err.Error(), http.StatusInternalServerError)
204 return
205 }
206 app, err := installer.NewDodoApp(cfg.Bytes())
207 if err != nil {
208 http.Error(w, err.Error(), http.StatusBadRequest)
209 return
210 }
gio218e8132025-04-22 17:11:58 +0000211 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400212 http.Error(w, err.Error(), http.StatusInternalServerError)
213 return
gioa421b062025-04-21 09:45:04 +0400214 } else {
gioda708652025-04-30 14:57:38 +0400215 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
216 if err != nil {
217 http.Error(w, err.Error(), http.StatusInternalServerError)
218 return
giof8acc612025-04-26 08:20:55 +0400219 }
gioda708652025-04-30 14:57:38 +0400220 s.im.Monitor(instanceId, outs)
gio218e8132025-04-22 17:11:58 +0000221 var cfg dodoAppRendered
222 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
223 http.Error(w, err.Error(), http.StatusInternalServerError)
224 }
225 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
226 Id: instanceId,
227 DeployKey: cfg.Input.Key.Public,
gio6ce44812025-05-17 07:31:54 +0400228 Access: rr.Access,
gio218e8132025-04-22 17:11:58 +0000229 }); err != nil {
230 http.Error(w, err.Error(), http.StatusInternalServerError)
231 }
giofc441e32024-11-11 16:26:14 +0400232 }
233}
234
gio63a1a822025-04-23 12:59:40 +0400235func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400236 s.l.Lock()
237 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400238 instanceId, ok := mux.Vars(r)["instanceId"]
239 if !ok {
240 http.Error(w, "missing instance id", http.StatusBadRequest)
241 }
gio268787a2025-04-24 21:18:06 +0400242 if _, ok := s.tasks[instanceId]; ok {
243 http.Error(w, "task in progress", http.StatusTooEarly)
244 return
245 }
gio63a1a822025-04-23 12:59:40 +0400246 var req dodoAppInstallReq
247 // TODO(gio): validate that no internal fields are overridden by request
248 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
249 http.Error(w, err.Error(), http.StatusBadRequest)
250 return
251 }
252 clusters, err := s.m.GetClusters()
253 if err != nil {
254 http.Error(w, err.Error(), http.StatusInternalServerError)
255 return
256 }
257 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
258 var cfg bytes.Buffer
259 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
260 http.Error(w, err.Error(), http.StatusInternalServerError)
261 return
262 }
263 overrides := installer.CueAppData{
264 "app.cue": cfg.Bytes(),
265 }
gio268787a2025-04-24 21:18:06 +0400266 rr, err := s.m.Update(instanceId, nil, overrides)
267 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400268 http.Error(w, err.Error(), http.StatusInternalServerError)
gioda708652025-04-30 14:57:38 +0400269 return
gio63a1a822025-04-23 12:59:40 +0400270 }
gioda708652025-04-30 14:57:38 +0400271 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
272 if err != nil {
273 http.Error(w, err.Error(), http.StatusInternalServerError)
274 return
giof8acc612025-04-26 08:20:55 +0400275 }
gioda708652025-04-30 14:57:38 +0400276 s.im.Monitor(instanceId, outs)
gio268787a2025-04-24 21:18:06 +0400277 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
278 if err == nil {
279 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
280 go s.reconciler.Reconcile(ctx)
281 }
282 return rr, err
283 })
284 if _, ok := s.tasks[instanceId]; ok {
285 panic("MUST NOT REACH!")
286 }
287 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
288 t.OnDone(s.cleanTask(instanceId, 0))
289 go t.Start()
gio6ce44812025-05-17 07:31:54 +0400290 var rend dodoAppRendered
291 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&rend); err != nil {
292 http.Error(w, err.Error(), http.StatusInternalServerError)
293 }
294 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
295 Id: instanceId,
296 DeployKey: rend.Input.Key.Public,
297 Access: rr.Access,
298 }); err != nil {
299 http.Error(w, err.Error(), http.StatusInternalServerError)
300 }
gio63a1a822025-04-23 12:59:40 +0400301}
302
gio59946282024-10-07 12:55:51 +0400303func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400304 env, err := s.m.Config()
305 if err != nil {
306 http.Error(w, err.Error(), http.StatusInternalServerError)
307 return
308 }
309 networks, err := s.m.CreateNetworks(env)
310 if err != nil {
311 http.Error(w, err.Error(), http.StatusInternalServerError)
312 return
313 }
314 if err := json.NewEncoder(w).Encode(networks); err != nil {
315 http.Error(w, err.Error(), http.StatusInternalServerError)
316 return
317 }
318}
319
gio59946282024-10-07 12:55:51 +0400320func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400321 clusters, err := s.m.GetClusters()
322 if err != nil {
323 http.Error(w, err.Error(), http.StatusInternalServerError)
324 return
325 }
326 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
327 http.Error(w, err.Error(), http.StatusInternalServerError)
328 return
329 }
330}
331
332type proxyPair struct {
333 From string `json:"from"`
334 To string `json:"to"`
335}
336
gio59946282024-10-07 12:55:51 +0400337func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400338 var req proxyPair
339 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
340 http.Error(w, err.Error(), http.StatusBadRequest)
341 return
342 }
gio721c0042025-04-03 11:56:36 +0400343 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400344 http.Error(w, err.Error(), http.StatusInternalServerError)
345 return
346 }
347}
348
gio59946282024-10-07 12:55:51 +0400349func (s *Server) handleProxyRemove(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.RemoveIngressProxy(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
361type app struct {
362 Name string `json:"name"`
363 Icon template.HTML `json:"icon"`
364 ShortDescription string `json:"shortDescription"`
365 Slug string `json:"slug"`
366 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
367}
368
gio59946282024-10-07 12:55:51 +0400369func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400370 all, err := s.r.GetAll()
371 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400372 http.Error(w, err.Error(), http.StatusInternalServerError)
373 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400374 }
375 resp := make([]app, len(all))
376 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400377 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400378 }
gioaa0fcdb2024-06-10 22:19:25 +0400379 w.Header().Set("Content-Type", "application/json")
380 if err := json.NewEncoder(w).Encode(resp); err != nil {
381 http.Error(w, err.Error(), http.StatusInternalServerError)
382 return
383 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400384}
385
gio59946282024-10-07 12:55:51 +0400386func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400387 slug, ok := mux.Vars(r)["slug"]
388 if !ok {
389 http.Error(w, "empty slug", http.StatusBadRequest)
390 return
391 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400392 a, err := s.r.Find(slug)
393 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400394 http.Error(w, err.Error(), http.StatusInternalServerError)
395 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400396 }
gio7fbd4ad2024-08-27 10:06:39 +0400397 instances, err := s.m.GetAllAppInstances(slug)
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 }
gioaa0fcdb2024-06-10 22:19:25 +0400402 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
403 w.Header().Set("Content-Type", "application/json")
404 if err := json.NewEncoder(w).Encode(resp); err != nil {
405 http.Error(w, err.Error(), http.StatusInternalServerError)
406 return
407 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400408}
409
gio59946282024-10-07 12:55:51 +0400410func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400411 slug, ok := mux.Vars(r)["slug"]
412 if !ok {
413 http.Error(w, "empty slug", http.StatusBadRequest)
414 return
415 }
gio7fbd4ad2024-08-27 10:06:39 +0400416 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400417 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400420 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400421 a, err := s.r.Find(instance.AppId)
422 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400423 http.Error(w, err.Error(), http.StatusInternalServerError)
424 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400425 }
gioaa0fcdb2024-06-10 22:19:25 +0400426 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
427 w.Header().Set("Content-Type", "application/json")
428 if err := json.NewEncoder(w).Encode(resp); err != nil {
429 http.Error(w, err.Error(), http.StatusInternalServerError)
430 return
431 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400432}
433
gio218e8132025-04-22 17:11:58 +0000434func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400435 env, err := s.m.Config()
436 if err != nil {
gio218e8132025-04-22 17:11:58 +0000437 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400438 }
439 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
440 suffix, err := suffixGen.Generate()
441 if err != nil {
gio218e8132025-04-22 17:11:58 +0000442 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400443 }
444 instanceId := app.Slug() + suffix
445 appDir := fmt.Sprintf("/apps/%s", instanceId)
446 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000447 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400448 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400449 if err == nil {
450 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
451 go s.reconciler.Reconcile(ctx)
452 }
453 return rr, err
454 })
455 if _, ok := s.tasks[instanceId]; ok {
456 panic("MUST NOT REACH!")
457 }
458 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
459 t.OnDone(s.cleanTask(instanceId, 0))
460 go t.Start()
gio218e8132025-04-22 17:11:58 +0000461 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400462}
463
gio59946282024-10-07 12:55:51 +0400464func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400465 s.l.Lock()
466 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400467 slug, ok := mux.Vars(r)["slug"]
468 if !ok {
469 http.Error(w, "empty slug", http.StatusBadRequest)
470 return
471 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400472 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400473 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400474 http.Error(w, err.Error(), http.StatusInternalServerError)
475 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400476 }
gioa421b062025-04-21 09:45:04 +0400477 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400478 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400479 http.Error(w, err.Error(), http.StatusInternalServerError)
480 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400481 }
gio218e8132025-04-22 17:11:58 +0000482 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400483 http.Error(w, err.Error(), http.StatusInternalServerError)
484 return
gioa421b062025-04-21 09:45:04 +0400485 } else {
486 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400487 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400488}
489
gio59946282024-10-07 12:55:51 +0400490func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400491 s.l.Lock()
492 defer s.l.Unlock()
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 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400498 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400499 if err := json.NewDecoder(r.Body).Decode(&values); 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 }
gio8c876172024-10-05 12:25:13 +0400503 tid := 0
504 if t, ok := s.tasks[slug]; ok {
505 if t.task != nil {
506 http.Error(w, "Update already in progress", http.StatusBadRequest)
507 return
508 }
509 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400510 }
gio63a1a822025-04-23 12:59:40 +0400511 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400512 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400513 http.Error(w, err.Error(), http.StatusInternalServerError)
514 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400515 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400516 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
517 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400518 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400519 t.OnDone(s.cleanTask(slug, tid))
520 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400521 go t.Start()
gio268787a2025-04-24 21:18:06 +0400522 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400523 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400524 http.Error(w, err.Error(), http.StatusInternalServerError)
525 return
526 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400527}
528
gio59946282024-10-07 12:55:51 +0400529func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400530 slug, ok := mux.Vars(r)["slug"]
531 if !ok {
532 http.Error(w, "empty slug", http.StatusBadRequest)
533 return
534 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400535 if err := s.m.Remove(slug); 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 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400539 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
540 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400541 if _, err := fmt.Fprint(w, "/"); err != nil {
542 http.Error(w, err.Error(), http.StatusInternalServerError)
543 return
544 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400545}
546
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400547type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400548 Apps []app
549 CurrentPage string
550 SearchTarget string
551 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400552}
553
gio59946282024-10-07 12:55:51 +0400554func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400555 pageType := mux.Vars(r)["pageType"]
556 if pageType == "" {
557 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400558 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400559 searchQuery := r.FormValue("query")
560 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400561 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400564 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400565 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400566 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400567 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400568 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400571 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400572 switch pageType {
573 case "installed":
574 if len(instances) != 0 {
575 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
576 }
577 case "not-installed":
578 if len(instances) == 0 {
579 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
580 }
581 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400582 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
583 }
584 }
585 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400586 Apps: resp,
587 CurrentPage: pageType,
588 SearchTarget: pageType,
589 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400590 }
gioaa0fcdb2024-06-10 22:19:25 +0400591 if err := s.tmpl.index.Execute(w, data); err != nil {
592 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400593 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400594}
595
596type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400597 App installer.EnvApp
598 Instance *installer.AppInstanceConfig
599 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400600 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400601 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400602 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400603}
604
gio59946282024-10-07 12:55:51 +0400605func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400606 global, err := s.m.Config()
607 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400608 http.Error(w, err.Error(), http.StatusInternalServerError)
609 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400610 }
gioaa0fcdb2024-06-10 22:19:25 +0400611 slug, ok := mux.Vars(r)["slug"]
612 if !ok {
613 http.Error(w, "empty slug", http.StatusBadRequest)
614 return
615 }
gio3cdee592024-04-17 10:15:56 +0400616 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400617 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400618 http.Error(w, err.Error(), http.StatusInternalServerError)
619 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400620 }
gio7fbd4ad2024-08-27 10:06:39 +0400621 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400622 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400623 http.Error(w, err.Error(), http.StatusInternalServerError)
624 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400625 }
giocb34ad22024-07-11 08:01:13 +0400626 networks, err := s.m.CreateNetworks(global)
627 if err != nil {
628 http.Error(w, err.Error(), http.StatusInternalServerError)
629 return
630 }
giof6ad2982024-08-23 17:42:49 +0400631 clusters, err := s.m.GetClusters()
632 if err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400636 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400637 App: a,
638 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400639 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400640 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400641 CurrentPage: a.Name(),
642 }
gioaa0fcdb2024-06-10 22:19:25 +0400643 if err := s.tmpl.app.Execute(w, data); err != nil {
644 http.Error(w, err.Error(), http.StatusInternalServerError)
645 return
646 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400647}
648
gio59946282024-10-07 12:55:51 +0400649func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400650 s.l.Lock()
651 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400652 global, err := s.m.Config()
653 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400654 http.Error(w, err.Error(), http.StatusInternalServerError)
655 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400656 }
gioaa0fcdb2024-06-10 22:19:25 +0400657 slug, ok := mux.Vars(r)["slug"]
658 if !ok {
659 http.Error(w, "empty slug", http.StatusBadRequest)
660 return
661 }
gio8c876172024-10-05 12:25:13 +0400662 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400663 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
664 return
665 }
gio8c876172024-10-05 12:25:13 +0400666 instance, err := s.m.GetInstance(slug)
667 if err != nil {
668 http.Error(w, err.Error(), http.StatusInternalServerError)
669 return
670 }
gio63a1a822025-04-23 12:59:40 +0400671 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400672 if err != nil {
673 http.Error(w, err.Error(), http.StatusInternalServerError)
674 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400675 }
gio7fbd4ad2024-08-27 10:06:39 +0400676 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400677 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400678 http.Error(w, err.Error(), http.StatusInternalServerError)
679 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400680 }
giocb34ad22024-07-11 08:01:13 +0400681 networks, err := s.m.CreateNetworks(global)
682 if err != nil {
683 http.Error(w, err.Error(), http.StatusInternalServerError)
684 return
685 }
giof6ad2982024-08-23 17:42:49 +0400686 clusters, err := s.m.GetClusters()
687 if err != nil {
688 http.Error(w, err.Error(), http.StatusInternalServerError)
689 return
690 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400691 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400692 App: a,
gio778577f2024-04-29 09:44:38 +0400693 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400694 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400695 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400696 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400697 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400698 }
gioaa0fcdb2024-06-10 22:19:25 +0400699 if err := s.tmpl.app.Execute(w, data); err != nil {
700 http.Error(w, err.Error(), http.StatusInternalServerError)
701 return
702 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400703}
giof6ad2982024-08-23 17:42:49 +0400704
705type taskStatusData struct {
706 CurrentPage string
707 Task tasks.Task
708}
709
gio59946282024-10-07 12:55:51 +0400710func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400711 s.l.Lock()
712 defer s.l.Unlock()
713 slug, ok := mux.Vars(r)["slug"]
714 if !ok {
715 http.Error(w, "empty slug", http.StatusBadRequest)
716 return
717 }
718 t, ok := s.tasks[slug]
719 if !ok {
720 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400721 return
722 }
gio8c876172024-10-05 12:25:13 +0400723 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400724 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
725 return
726 }
727 data := taskStatusData{
728 CurrentPage: "",
729 Task: t.task,
730 }
731 if err := s.tmpl.task.Execute(w, data); err != nil {
732 http.Error(w, err.Error(), http.StatusInternalServerError)
733 return
734 }
735}
736
gio268787a2025-04-24 21:18:06 +0400737type resourceStatus struct {
738 Type string `json:"type"`
739 Name string `json:"name"`
740 Status string `json:"status"`
741}
742
743func extractResources(t tasks.Task) []resourceStatus {
744 var ret []resourceStatus
745 if t.Resource() != nil {
746 ret = append(ret, resourceStatus{
747 Type: t.Resource().Type,
748 Name: t.Resource().Name,
749 Status: tasks.StatusString(t.Status()),
750 })
751 }
752 for _, st := range t.Subtasks() {
753 ret = append(ret, extractResources(st)...)
754 }
755 return ret
756}
757
giof8acc612025-04-26 08:20:55 +0400758func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400759 s.l.Lock()
760 defer s.l.Unlock()
761 instanceId, ok := mux.Vars(r)["instanceId"]
762 if !ok {
763 http.Error(w, "empty slug", http.StatusBadRequest)
764 return
765 }
giof8acc612025-04-26 08:20:55 +0400766 statuses, err := s.im.Get(instanceId)
767 if err != nil {
768 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400769 return
770 }
gioda708652025-04-30 14:57:38 +0400771 ret := []resourceStatus{}
772 for r, s := range statuses {
773 ret = append(ret, resourceStatus{
774 Type: r.Type,
775 Name: r.Name,
776 Status: status.StatusString(s),
giof8acc612025-04-26 08:20:55 +0400777 })
778 }
gioda708652025-04-30 14:57:38 +0400779 json.NewEncoder(w).Encode(ret)
gio268787a2025-04-24 21:18:06 +0400780}
781
giof6ad2982024-08-23 17:42:49 +0400782type clustersData struct {
783 CurrentPage string
784 Clusters []cluster.State
785}
786
gio59946282024-10-07 12:55:51 +0400787func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400788 clusters, err := s.m.GetClusters()
789 if err != nil {
790 http.Error(w, err.Error(), http.StatusInternalServerError)
791 return
792 }
793 data := clustersData{
794 "clusters",
795 clusters,
796 }
797 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
798 http.Error(w, err.Error(), http.StatusInternalServerError)
799 return
800 }
801}
802
803type clusterData struct {
804 CurrentPage string
805 Cluster cluster.State
806}
807
gio59946282024-10-07 12:55:51 +0400808func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400809 name, ok := mux.Vars(r)["name"]
810 if !ok {
811 http.Error(w, "empty name", http.StatusBadRequest)
812 return
813 }
814 m, err := s.getClusterManager(name)
815 if err != nil {
816 if errors.Is(err, installer.ErrorNotFound) {
817 http.Error(w, "not found", http.StatusNotFound)
818 } else {
819 http.Error(w, err.Error(), http.StatusInternalServerError)
820 }
821 return
822 }
823 data := clusterData{
824 "clusters",
825 m.State(),
826 }
827 if err := s.tmpl.cluster.Execute(w, data); err != nil {
828 http.Error(w, err.Error(), http.StatusInternalServerError)
829 return
830 }
831}
832
gio59946282024-10-07 12:55:51 +0400833func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400834 cName, ok := mux.Vars(r)["name"]
835 if !ok {
836 http.Error(w, "empty name", http.StatusBadRequest)
837 return
838 }
gio8c876172024-10-05 12:25:13 +0400839 tid := 0
840 if t, ok := s.tasks[cName]; ok {
841 if t.task != nil {
842 http.Error(w, "cluster task in progress", http.StatusLocked)
843 return
844 }
845 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400846 }
847 m, err := s.getClusterManager(cName)
848 if err != nil {
849 if errors.Is(err, installer.ErrorNotFound) {
850 http.Error(w, "not found", http.StatusNotFound)
851 } else {
852 http.Error(w, err.Error(), http.StatusInternalServerError)
853 }
854 return
855 }
856 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400857 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400858 go task.Start()
gio8c876172024-10-05 12:25:13 +0400859 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400860 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
861}
862
gio59946282024-10-07 12:55:51 +0400863func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400864 s.l.Lock()
865 defer s.l.Unlock()
866 cName, ok := mux.Vars(r)["cluster"]
867 if !ok {
868 http.Error(w, "empty name", http.StatusBadRequest)
869 return
870 }
gio8c876172024-10-05 12:25:13 +0400871 tid := 0
872 if t, ok := s.tasks[cName]; ok {
873 if t.task != nil {
874 http.Error(w, "cluster task in progress", http.StatusLocked)
875 return
876 }
877 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400878 }
879 sName, ok := mux.Vars(r)["server"]
880 if !ok {
881 http.Error(w, "empty name", http.StatusBadRequest)
882 return
883 }
884 m, err := s.getClusterManager(cName)
885 if err != nil {
886 if errors.Is(err, installer.ErrorNotFound) {
887 http.Error(w, "not found", http.StatusNotFound)
888 } else {
889 http.Error(w, err.Error(), http.StatusInternalServerError)
890 }
891 return
892 }
893 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400894 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400895 go task.Start()
gio8c876172024-10-05 12:25:13 +0400896 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400897 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
898}
899
gio59946282024-10-07 12:55:51 +0400900func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400901 clusters, err := s.m.GetClusters()
902 if err != nil {
903 return nil, err
904 }
905 var c *cluster.State
906 for _, i := range clusters {
907 if i.Name == cName {
908 c = &i
909 break
910 }
911 }
912 if c == nil {
913 return nil, installer.ErrorNotFound
914 }
915 return cluster.RestoreKubeManager(*c)
916}
917
gio59946282024-10-07 12:55:51 +0400918func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400919 s.l.Lock()
920 defer s.l.Unlock()
921 cName, ok := mux.Vars(r)["cluster"]
922 if !ok {
923 http.Error(w, "empty name", http.StatusBadRequest)
924 return
925 }
gio8c876172024-10-05 12:25:13 +0400926 tid := 0
927 if t, ok := s.tasks[cName]; ok {
928 if t.task != nil {
929 http.Error(w, "cluster task in progress", http.StatusLocked)
930 return
931 }
932 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400933 }
934 m, err := s.getClusterManager(cName)
935 if err != nil {
936 if errors.Is(err, installer.ErrorNotFound) {
937 http.Error(w, "not found", http.StatusNotFound)
938 } else {
939 http.Error(w, err.Error(), http.StatusInternalServerError)
940 }
941 return
942 }
943 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400944 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400945 if ip == nil {
946 http.Error(w, "invalid ip", http.StatusBadRequest)
947 return
948 }
949 port := 22
950 if p := r.PostFormValue("port"); p != "" {
951 port, err = strconv.Atoi(p)
952 if err != nil {
953 http.Error(w, err.Error(), http.StatusBadRequest)
954 return
955 }
956 }
957 server := cluster.Server{
958 IP: ip,
959 Port: port,
960 User: r.PostFormValue("user"),
961 Password: r.PostFormValue("password"),
962 }
963 var task tasks.Task
964 switch strings.ToLower(t) {
965 case "controller":
966 if len(m.State().Controllers) == 0 {
967 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
968 } else {
969 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
970 }
971 case "worker":
972 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
973 default:
974 http.Error(w, "invalid type", http.StatusBadRequest)
975 return
976 }
gio8c876172024-10-05 12:25:13 +0400977 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400978 go task.Start()
gio8c876172024-10-05 12:25:13 +0400979 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400980 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
981}
982
gio59946282024-10-07 12:55:51 +0400983func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400984 cName := r.PostFormValue("name")
985 if cName == "" {
986 http.Error(w, "no name", http.StatusBadRequest)
987 return
988 }
989 st := cluster.State{Name: cName}
990 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
991 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
992 return "", err
993 }
994 return fmt.Sprintf("create cluster: %s", cName), nil
995 }); err != nil {
996 http.Error(w, err.Error(), http.StatusInternalServerError)
997 return
998 }
999 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
1000}
1001
gio59946282024-10-07 12:55:51 +04001002func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001003 cName, ok := mux.Vars(r)["name"]
1004 if !ok {
1005 http.Error(w, "empty name", http.StatusBadRequest)
1006 return
1007 }
gio8c876172024-10-05 12:25:13 +04001008 tid := 0
1009 if t, ok := s.tasks[cName]; ok {
1010 if t.task != nil {
1011 http.Error(w, "cluster task in progress", http.StatusLocked)
1012 return
1013 }
1014 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001015 }
1016 m, err := s.getClusterManager(cName)
1017 if err != nil {
1018 if errors.Is(err, installer.ErrorNotFound) {
1019 http.Error(w, "not found", http.StatusNotFound)
1020 } else {
1021 http.Error(w, err.Error(), http.StatusInternalServerError)
1022 }
1023 return
1024 }
1025 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +04001026 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001027 go task.Start()
gio8c876172024-10-05 12:25:13 +04001028 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001029 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1030}
1031
gio59946282024-10-07 12:55:51 +04001032func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001033 const vpnUser = "private-network-proxy"
1034 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1035 hostname := fmt.Sprintf("cluster-%s", name)
1036 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1037 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1038 if err != nil {
1039 return installer.ReleaseResources{}, err
1040 }
1041 env, err := s.m.Config()
1042 if err != nil {
1043 return installer.ReleaseResources{}, err
1044 }
gio721c0042025-04-03 11:56:36 +04001045 keys, err := installer.NewSSHKeyPair("port-allocator")
1046 if err != nil {
1047 return installer.ReleaseResources{}, err
1048 }
1049 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1050 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1051 return installer.ReleaseResources{}, err
1052 }
1053 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1054 return installer.ReleaseResources{}, err
1055 }
giof6ad2982024-08-23 17:42:49 +04001056 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1057 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001058 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001059 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1060 "cluster": map[string]any{
1061 "name": name,
1062 "kubeconfig": kubeconfig,
1063 "ingressClassName": ingressClassName,
1064 },
1065 // TODO(gio): remove hardcoded user
1066 "vpnUser": vpnUser,
1067 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001068 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001069 })
1070 if err != nil {
1071 return installer.ReleaseResources{}, err
1072 }
1073 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1074 go s.reconciler.Reconcile(ctx)
1075 return rr, err
1076 })
1077 ch := make(chan error)
1078 t.OnDone(func(err error) {
1079 ch <- err
1080 })
1081 go t.Start()
1082 err := <-ch
1083 if err != nil {
1084 return nil, err
1085 }
1086 for {
1087 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1088 if err == nil {
1089 return ip, nil
1090 }
1091 if errors.Is(err, installer.ErrorNotFound) {
1092 time.Sleep(5 * time.Second)
1093 }
1094 }
1095 }
1096}
gio8f290322024-09-21 15:37:45 +04001097
gio59946282024-10-07 12:55:51 +04001098func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001099 return func(cm cluster.Manager) error {
1100 name := cm.State().Name
1101 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1102 app, err := installer.FindEnvApp(s.fr, "longhorn")
1103 if err != nil {
1104 return installer.ReleaseResources{}, err
1105 }
1106 env, err := s.m.Config()
1107 if err != nil {
1108 return installer.ReleaseResources{}, err
1109 }
1110 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1111 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1112 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1113 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1114 "cluster": name,
1115 })
1116 if err != nil {
1117 return installer.ReleaseResources{}, err
1118 }
1119 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1120 go s.reconciler.Reconcile(ctx)
1121 return rr, err
1122 })
1123 ch := make(chan error)
1124 t.OnDone(func(err error) {
1125 ch <- err
1126 })
1127 go t.Start()
1128 err := <-ch
1129 if err != nil {
1130 return err
1131 }
1132 cm.EnableStorage()
1133 return nil
1134 }
1135}
gio8c876172024-10-05 12:25:13 +04001136
gio59946282024-10-07 12:55:51 +04001137func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001138 return func(err error) {
1139 if err != nil {
1140 fmt.Printf("Task %s failed: %s", name, err.Error())
1141 }
1142 s.l.Lock()
1143 defer s.l.Unlock()
1144 s.tasks[name].task = nil
1145 go func() {
1146 time.Sleep(30 * time.Second)
1147 s.l.Lock()
1148 defer s.l.Unlock()
1149 if t, ok := s.tasks[name]; ok && t.id == id {
1150 delete(s.tasks, name)
1151 }
1152 }()
1153 }
1154}