blob: e0aeab2a2141be94bc4506c228760fb300f739e2 [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 {
173 Id string `json:"id"`
174 DeployKey string `json:"deployKey"`
175}
176
177type dodoAppRendered struct {
178 Input struct {
179 Key struct {
180 Public string `json:"public"`
181 } `json:"key"`
182 } `json:"input"`
183}
184
giofc441e32024-11-11 16:26:14 +0400185func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400186 s.l.Lock()
187 defer s.l.Unlock()
giofc441e32024-11-11 16:26:14 +0400188 var req dodoAppInstallReq
189 // TODO(gio): validate that no internal fields are overridden by request
190 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
191 http.Error(w, err.Error(), http.StatusBadRequest)
192 return
193 }
194 clusters, err := s.m.GetClusters()
195 if err != nil {
196 http.Error(w, err.Error(), http.StatusInternalServerError)
197 return
198 }
199 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
200 var cfg bytes.Buffer
201 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
202 http.Error(w, err.Error(), http.StatusInternalServerError)
203 return
204 }
205 app, err := installer.NewDodoApp(cfg.Bytes())
206 if err != nil {
207 http.Error(w, err.Error(), http.StatusBadRequest)
208 return
209 }
gio218e8132025-04-22 17:11:58 +0000210 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400211 http.Error(w, err.Error(), http.StatusInternalServerError)
212 return
gioa421b062025-04-21 09:45:04 +0400213 } else {
gioda708652025-04-30 14:57:38 +0400214 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
215 if err != nil {
216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
giof8acc612025-04-26 08:20:55 +0400218 }
gioda708652025-04-30 14:57:38 +0400219 s.im.Monitor(instanceId, outs)
gio218e8132025-04-22 17:11:58 +0000220 var cfg dodoAppRendered
221 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 }
224 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
225 Id: instanceId,
226 DeployKey: cfg.Input.Key.Public,
227 }); err != nil {
228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 }
giofc441e32024-11-11 16:26:14 +0400230 }
231}
232
gio63a1a822025-04-23 12:59:40 +0400233func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400234 s.l.Lock()
235 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400236 instanceId, ok := mux.Vars(r)["instanceId"]
237 if !ok {
238 http.Error(w, "missing instance id", http.StatusBadRequest)
239 }
gio268787a2025-04-24 21:18:06 +0400240 if _, ok := s.tasks[instanceId]; ok {
241 http.Error(w, "task in progress", http.StatusTooEarly)
242 return
243 }
gio63a1a822025-04-23 12:59:40 +0400244 var req dodoAppInstallReq
245 // TODO(gio): validate that no internal fields are overridden by request
246 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
247 http.Error(w, err.Error(), http.StatusBadRequest)
248 return
249 }
250 clusters, err := s.m.GetClusters()
251 if err != nil {
252 http.Error(w, err.Error(), http.StatusInternalServerError)
253 return
254 }
255 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
256 var cfg bytes.Buffer
257 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
258 http.Error(w, err.Error(), http.StatusInternalServerError)
259 return
260 }
261 overrides := installer.CueAppData{
262 "app.cue": cfg.Bytes(),
263 }
gio268787a2025-04-24 21:18:06 +0400264 rr, err := s.m.Update(instanceId, nil, overrides)
265 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400266 http.Error(w, err.Error(), http.StatusInternalServerError)
gioda708652025-04-30 14:57:38 +0400267 return
gio63a1a822025-04-23 12:59:40 +0400268 }
gioda708652025-04-30 14:57:38 +0400269 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
270 if err != nil {
271 http.Error(w, err.Error(), http.StatusInternalServerError)
272 return
giof8acc612025-04-26 08:20:55 +0400273 }
gioda708652025-04-30 14:57:38 +0400274 s.im.Monitor(instanceId, outs)
gio268787a2025-04-24 21:18:06 +0400275 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
276 if err == nil {
277 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
278 go s.reconciler.Reconcile(ctx)
279 }
280 return rr, err
281 })
282 if _, ok := s.tasks[instanceId]; ok {
283 panic("MUST NOT REACH!")
284 }
285 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
286 t.OnDone(s.cleanTask(instanceId, 0))
287 go t.Start()
gio63a1a822025-04-23 12:59:40 +0400288}
289
gio59946282024-10-07 12:55:51 +0400290func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400291 env, err := s.m.Config()
292 if err != nil {
293 http.Error(w, err.Error(), http.StatusInternalServerError)
294 return
295 }
296 networks, err := s.m.CreateNetworks(env)
297 if err != nil {
298 http.Error(w, err.Error(), http.StatusInternalServerError)
299 return
300 }
301 if err := json.NewEncoder(w).Encode(networks); err != nil {
302 http.Error(w, err.Error(), http.StatusInternalServerError)
303 return
304 }
305}
306
gio59946282024-10-07 12:55:51 +0400307func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400308 clusters, err := s.m.GetClusters()
309 if err != nil {
310 http.Error(w, err.Error(), http.StatusInternalServerError)
311 return
312 }
313 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
314 http.Error(w, err.Error(), http.StatusInternalServerError)
315 return
316 }
317}
318
319type proxyPair struct {
320 From string `json:"from"`
321 To string `json:"to"`
322}
323
gio59946282024-10-07 12:55:51 +0400324func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400325 var req proxyPair
326 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
327 http.Error(w, err.Error(), http.StatusBadRequest)
328 return
329 }
gio721c0042025-04-03 11:56:36 +0400330 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400331 http.Error(w, err.Error(), http.StatusInternalServerError)
332 return
333 }
334}
335
gio59946282024-10-07 12:55:51 +0400336func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400337 var req proxyPair
338 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
339 http.Error(w, err.Error(), http.StatusBadRequest)
340 return
341 }
gio721c0042025-04-03 11:56:36 +0400342 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400343 http.Error(w, err.Error(), http.StatusInternalServerError)
344 return
345 }
346}
347
348type app struct {
349 Name string `json:"name"`
350 Icon template.HTML `json:"icon"`
351 ShortDescription string `json:"shortDescription"`
352 Slug string `json:"slug"`
353 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
354}
355
gio59946282024-10-07 12:55:51 +0400356func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400357 all, err := s.r.GetAll()
358 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400361 }
362 resp := make([]app, len(all))
363 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400364 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400365 }
gioaa0fcdb2024-06-10 22:19:25 +0400366 w.Header().Set("Content-Type", "application/json")
367 if err := json.NewEncoder(w).Encode(resp); err != nil {
368 http.Error(w, err.Error(), http.StatusInternalServerError)
369 return
370 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400371}
372
gio59946282024-10-07 12:55:51 +0400373func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400374 slug, ok := mux.Vars(r)["slug"]
375 if !ok {
376 http.Error(w, "empty slug", http.StatusBadRequest)
377 return
378 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400379 a, err := s.r.Find(slug)
380 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400381 http.Error(w, err.Error(), http.StatusInternalServerError)
382 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400383 }
gio7fbd4ad2024-08-27 10:06:39 +0400384 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400385 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400386 http.Error(w, err.Error(), http.StatusInternalServerError)
387 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400388 }
gioaa0fcdb2024-06-10 22:19:25 +0400389 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
390 w.Header().Set("Content-Type", "application/json")
391 if err := json.NewEncoder(w).Encode(resp); err != nil {
392 http.Error(w, err.Error(), http.StatusInternalServerError)
393 return
394 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395}
396
gio59946282024-10-07 12:55:51 +0400397func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400398 slug, ok := mux.Vars(r)["slug"]
399 if !ok {
400 http.Error(w, "empty slug", http.StatusBadRequest)
401 return
402 }
gio7fbd4ad2024-08-27 10:06:39 +0400403 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400404 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400405 http.Error(w, err.Error(), http.StatusInternalServerError)
406 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400407 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400408 a, err := s.r.Find(instance.AppId)
409 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400410 http.Error(w, err.Error(), http.StatusInternalServerError)
411 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400412 }
gioaa0fcdb2024-06-10 22:19:25 +0400413 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
414 w.Header().Set("Content-Type", "application/json")
415 if err := json.NewEncoder(w).Encode(resp); err != nil {
416 http.Error(w, err.Error(), http.StatusInternalServerError)
417 return
418 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400419}
420
gio218e8132025-04-22 17:11:58 +0000421func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400422 env, err := s.m.Config()
423 if err != nil {
gio218e8132025-04-22 17:11:58 +0000424 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400425 }
426 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
427 suffix, err := suffixGen.Generate()
428 if err != nil {
gio218e8132025-04-22 17:11:58 +0000429 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400430 }
431 instanceId := app.Slug() + suffix
432 appDir := fmt.Sprintf("/apps/%s", instanceId)
433 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000434 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400435 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400436 if err == nil {
437 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
438 go s.reconciler.Reconcile(ctx)
439 }
440 return rr, err
441 })
442 if _, ok := s.tasks[instanceId]; ok {
443 panic("MUST NOT REACH!")
444 }
445 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
446 t.OnDone(s.cleanTask(instanceId, 0))
447 go t.Start()
gio218e8132025-04-22 17:11:58 +0000448 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400449}
450
gio59946282024-10-07 12:55:51 +0400451func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400452 s.l.Lock()
453 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400454 slug, ok := mux.Vars(r)["slug"]
455 if !ok {
456 http.Error(w, "empty slug", http.StatusBadRequest)
457 return
458 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400459 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400460 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400461 http.Error(w, err.Error(), http.StatusInternalServerError)
462 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400463 }
gioa421b062025-04-21 09:45:04 +0400464 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400465 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400466 http.Error(w, err.Error(), http.StatusInternalServerError)
467 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400468 }
gio218e8132025-04-22 17:11:58 +0000469 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400470 http.Error(w, err.Error(), http.StatusInternalServerError)
471 return
gioa421b062025-04-21 09:45:04 +0400472 } else {
473 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400474 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400475}
476
gio59946282024-10-07 12:55:51 +0400477func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400478 s.l.Lock()
479 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400480 slug, ok := mux.Vars(r)["slug"]
481 if !ok {
482 http.Error(w, "empty slug", http.StatusBadRequest)
483 return
484 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400485 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400486 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400487 http.Error(w, err.Error(), http.StatusInternalServerError)
488 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400489 }
gio8c876172024-10-05 12:25:13 +0400490 tid := 0
491 if t, ok := s.tasks[slug]; ok {
492 if t.task != nil {
493 http.Error(w, "Update already in progress", http.StatusBadRequest)
494 return
495 }
496 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400497 }
gio63a1a822025-04-23 12:59:40 +0400498 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400499 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400500 http.Error(w, err.Error(), http.StatusInternalServerError)
501 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400502 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400503 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
504 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400505 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400506 t.OnDone(s.cleanTask(slug, tid))
507 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400508 go t.Start()
gio268787a2025-04-24 21:18:06 +0400509 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400510 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400511 http.Error(w, err.Error(), http.StatusInternalServerError)
512 return
513 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400514}
515
gio59946282024-10-07 12:55:51 +0400516func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400517 slug, ok := mux.Vars(r)["slug"]
518 if !ok {
519 http.Error(w, "empty slug", http.StatusBadRequest)
520 return
521 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400522 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400523 http.Error(w, err.Error(), http.StatusInternalServerError)
524 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400525 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400526 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
527 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400528 if _, err := fmt.Fprint(w, "/"); err != nil {
529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400532}
533
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400534type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400535 Apps []app
536 CurrentPage string
537 SearchTarget string
538 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400539}
540
gio59946282024-10-07 12:55:51 +0400541func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400542 pageType := mux.Vars(r)["pageType"]
543 if pageType == "" {
544 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400545 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400546 searchQuery := r.FormValue("query")
547 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400548 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400551 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400552 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400553 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400554 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400555 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400556 http.Error(w, err.Error(), http.StatusInternalServerError)
557 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400558 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400559 switch pageType {
560 case "installed":
561 if len(instances) != 0 {
562 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
563 }
564 case "not-installed":
565 if len(instances) == 0 {
566 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
567 }
568 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400569 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
570 }
571 }
572 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400573 Apps: resp,
574 CurrentPage: pageType,
575 SearchTarget: pageType,
576 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400577 }
gioaa0fcdb2024-06-10 22:19:25 +0400578 if err := s.tmpl.index.Execute(w, data); err != nil {
579 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400580 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400581}
582
583type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400584 App installer.EnvApp
585 Instance *installer.AppInstanceConfig
586 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400587 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400588 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400589 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400590}
591
gio59946282024-10-07 12:55:51 +0400592func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400593 global, err := s.m.Config()
594 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400595 http.Error(w, err.Error(), http.StatusInternalServerError)
596 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400597 }
gioaa0fcdb2024-06-10 22:19:25 +0400598 slug, ok := mux.Vars(r)["slug"]
599 if !ok {
600 http.Error(w, "empty slug", http.StatusBadRequest)
601 return
602 }
gio3cdee592024-04-17 10:15:56 +0400603 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400604 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400605 http.Error(w, err.Error(), http.StatusInternalServerError)
606 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400607 }
gio7fbd4ad2024-08-27 10:06:39 +0400608 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400609 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400610 http.Error(w, err.Error(), http.StatusInternalServerError)
611 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400612 }
giocb34ad22024-07-11 08:01:13 +0400613 networks, err := s.m.CreateNetworks(global)
614 if err != nil {
615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
617 }
giof6ad2982024-08-23 17:42:49 +0400618 clusters, err := s.m.GetClusters()
619 if err != nil {
620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
622 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400623 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400624 App: a,
625 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400626 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400627 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400628 CurrentPage: a.Name(),
629 }
gioaa0fcdb2024-06-10 22:19:25 +0400630 if err := s.tmpl.app.Execute(w, data); err != nil {
631 http.Error(w, err.Error(), http.StatusInternalServerError)
632 return
633 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400634}
635
gio59946282024-10-07 12:55:51 +0400636func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400637 s.l.Lock()
638 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400639 global, err := s.m.Config()
640 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400641 http.Error(w, err.Error(), http.StatusInternalServerError)
642 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400643 }
gioaa0fcdb2024-06-10 22:19:25 +0400644 slug, ok := mux.Vars(r)["slug"]
645 if !ok {
646 http.Error(w, "empty slug", http.StatusBadRequest)
647 return
648 }
gio8c876172024-10-05 12:25:13 +0400649 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400650 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
651 return
652 }
gio8c876172024-10-05 12:25:13 +0400653 instance, err := s.m.GetInstance(slug)
654 if err != nil {
655 http.Error(w, err.Error(), http.StatusInternalServerError)
656 return
657 }
gio63a1a822025-04-23 12:59:40 +0400658 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400659 if err != nil {
660 http.Error(w, err.Error(), http.StatusInternalServerError)
661 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400662 }
gio7fbd4ad2024-08-27 10:06:39 +0400663 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400664 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400665 http.Error(w, err.Error(), http.StatusInternalServerError)
666 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400667 }
giocb34ad22024-07-11 08:01:13 +0400668 networks, err := s.m.CreateNetworks(global)
669 if err != nil {
670 http.Error(w, err.Error(), http.StatusInternalServerError)
671 return
672 }
giof6ad2982024-08-23 17:42:49 +0400673 clusters, err := s.m.GetClusters()
674 if err != nil {
675 http.Error(w, err.Error(), http.StatusInternalServerError)
676 return
677 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400678 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400679 App: a,
gio778577f2024-04-29 09:44:38 +0400680 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400681 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400682 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400683 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400684 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400685 }
gioaa0fcdb2024-06-10 22:19:25 +0400686 if err := s.tmpl.app.Execute(w, data); err != nil {
687 http.Error(w, err.Error(), http.StatusInternalServerError)
688 return
689 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400690}
giof6ad2982024-08-23 17:42:49 +0400691
692type taskStatusData struct {
693 CurrentPage string
694 Task tasks.Task
695}
696
gio59946282024-10-07 12:55:51 +0400697func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400698 s.l.Lock()
699 defer s.l.Unlock()
700 slug, ok := mux.Vars(r)["slug"]
701 if !ok {
702 http.Error(w, "empty slug", http.StatusBadRequest)
703 return
704 }
705 t, ok := s.tasks[slug]
706 if !ok {
707 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400708 return
709 }
gio8c876172024-10-05 12:25:13 +0400710 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400711 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
712 return
713 }
714 data := taskStatusData{
715 CurrentPage: "",
716 Task: t.task,
717 }
718 if err := s.tmpl.task.Execute(w, data); err != nil {
719 http.Error(w, err.Error(), http.StatusInternalServerError)
720 return
721 }
722}
723
gio268787a2025-04-24 21:18:06 +0400724type resourceStatus struct {
725 Type string `json:"type"`
726 Name string `json:"name"`
727 Status string `json:"status"`
728}
729
730func extractResources(t tasks.Task) []resourceStatus {
731 var ret []resourceStatus
732 if t.Resource() != nil {
733 ret = append(ret, resourceStatus{
734 Type: t.Resource().Type,
735 Name: t.Resource().Name,
736 Status: tasks.StatusString(t.Status()),
737 })
738 }
739 for _, st := range t.Subtasks() {
740 ret = append(ret, extractResources(st)...)
741 }
742 return ret
743}
744
giof8acc612025-04-26 08:20:55 +0400745func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400746 s.l.Lock()
747 defer s.l.Unlock()
748 instanceId, ok := mux.Vars(r)["instanceId"]
749 if !ok {
750 http.Error(w, "empty slug", http.StatusBadRequest)
751 return
752 }
giof8acc612025-04-26 08:20:55 +0400753 statuses, err := s.im.Get(instanceId)
754 if err != nil {
755 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400756 return
757 }
gioda708652025-04-30 14:57:38 +0400758 ret := []resourceStatus{}
759 for r, s := range statuses {
760 ret = append(ret, resourceStatus{
761 Type: r.Type,
762 Name: r.Name,
763 Status: status.StatusString(s),
giof8acc612025-04-26 08:20:55 +0400764 })
765 }
gioda708652025-04-30 14:57:38 +0400766 json.NewEncoder(w).Encode(ret)
gio268787a2025-04-24 21:18:06 +0400767}
768
giof6ad2982024-08-23 17:42:49 +0400769type clustersData struct {
770 CurrentPage string
771 Clusters []cluster.State
772}
773
gio59946282024-10-07 12:55:51 +0400774func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400775 clusters, err := s.m.GetClusters()
776 if err != nil {
777 http.Error(w, err.Error(), http.StatusInternalServerError)
778 return
779 }
780 data := clustersData{
781 "clusters",
782 clusters,
783 }
784 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
785 http.Error(w, err.Error(), http.StatusInternalServerError)
786 return
787 }
788}
789
790type clusterData struct {
791 CurrentPage string
792 Cluster cluster.State
793}
794
gio59946282024-10-07 12:55:51 +0400795func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400796 name, ok := mux.Vars(r)["name"]
797 if !ok {
798 http.Error(w, "empty name", http.StatusBadRequest)
799 return
800 }
801 m, err := s.getClusterManager(name)
802 if err != nil {
803 if errors.Is(err, installer.ErrorNotFound) {
804 http.Error(w, "not found", http.StatusNotFound)
805 } else {
806 http.Error(w, err.Error(), http.StatusInternalServerError)
807 }
808 return
809 }
810 data := clusterData{
811 "clusters",
812 m.State(),
813 }
814 if err := s.tmpl.cluster.Execute(w, data); err != nil {
815 http.Error(w, err.Error(), http.StatusInternalServerError)
816 return
817 }
818}
819
gio59946282024-10-07 12:55:51 +0400820func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400821 cName, ok := mux.Vars(r)["name"]
822 if !ok {
823 http.Error(w, "empty name", http.StatusBadRequest)
824 return
825 }
gio8c876172024-10-05 12:25:13 +0400826 tid := 0
827 if t, ok := s.tasks[cName]; ok {
828 if t.task != nil {
829 http.Error(w, "cluster task in progress", http.StatusLocked)
830 return
831 }
832 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400833 }
834 m, err := s.getClusterManager(cName)
835 if err != nil {
836 if errors.Is(err, installer.ErrorNotFound) {
837 http.Error(w, "not found", http.StatusNotFound)
838 } else {
839 http.Error(w, err.Error(), http.StatusInternalServerError)
840 }
841 return
842 }
843 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400844 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400845 go task.Start()
gio8c876172024-10-05 12:25:13 +0400846 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400847 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
848}
849
gio59946282024-10-07 12:55:51 +0400850func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400851 s.l.Lock()
852 defer s.l.Unlock()
853 cName, ok := mux.Vars(r)["cluster"]
854 if !ok {
855 http.Error(w, "empty name", http.StatusBadRequest)
856 return
857 }
gio8c876172024-10-05 12:25:13 +0400858 tid := 0
859 if t, ok := s.tasks[cName]; ok {
860 if t.task != nil {
861 http.Error(w, "cluster task in progress", http.StatusLocked)
862 return
863 }
864 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400865 }
866 sName, ok := mux.Vars(r)["server"]
867 if !ok {
868 http.Error(w, "empty name", http.StatusBadRequest)
869 return
870 }
871 m, err := s.getClusterManager(cName)
872 if err != nil {
873 if errors.Is(err, installer.ErrorNotFound) {
874 http.Error(w, "not found", http.StatusNotFound)
875 } else {
876 http.Error(w, err.Error(), http.StatusInternalServerError)
877 }
878 return
879 }
880 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400881 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400882 go task.Start()
gio8c876172024-10-05 12:25:13 +0400883 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400884 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
885}
886
gio59946282024-10-07 12:55:51 +0400887func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400888 clusters, err := s.m.GetClusters()
889 if err != nil {
890 return nil, err
891 }
892 var c *cluster.State
893 for _, i := range clusters {
894 if i.Name == cName {
895 c = &i
896 break
897 }
898 }
899 if c == nil {
900 return nil, installer.ErrorNotFound
901 }
902 return cluster.RestoreKubeManager(*c)
903}
904
gio59946282024-10-07 12:55:51 +0400905func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400906 s.l.Lock()
907 defer s.l.Unlock()
908 cName, ok := mux.Vars(r)["cluster"]
909 if !ok {
910 http.Error(w, "empty name", http.StatusBadRequest)
911 return
912 }
gio8c876172024-10-05 12:25:13 +0400913 tid := 0
914 if t, ok := s.tasks[cName]; ok {
915 if t.task != nil {
916 http.Error(w, "cluster task in progress", http.StatusLocked)
917 return
918 }
919 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400920 }
921 m, err := s.getClusterManager(cName)
922 if err != nil {
923 if errors.Is(err, installer.ErrorNotFound) {
924 http.Error(w, "not found", http.StatusNotFound)
925 } else {
926 http.Error(w, err.Error(), http.StatusInternalServerError)
927 }
928 return
929 }
930 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400931 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400932 if ip == nil {
933 http.Error(w, "invalid ip", http.StatusBadRequest)
934 return
935 }
936 port := 22
937 if p := r.PostFormValue("port"); p != "" {
938 port, err = strconv.Atoi(p)
939 if err != nil {
940 http.Error(w, err.Error(), http.StatusBadRequest)
941 return
942 }
943 }
944 server := cluster.Server{
945 IP: ip,
946 Port: port,
947 User: r.PostFormValue("user"),
948 Password: r.PostFormValue("password"),
949 }
950 var task tasks.Task
951 switch strings.ToLower(t) {
952 case "controller":
953 if len(m.State().Controllers) == 0 {
954 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
955 } else {
956 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
957 }
958 case "worker":
959 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
960 default:
961 http.Error(w, "invalid type", http.StatusBadRequest)
962 return
963 }
gio8c876172024-10-05 12:25:13 +0400964 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400965 go task.Start()
gio8c876172024-10-05 12:25:13 +0400966 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400967 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
968}
969
gio59946282024-10-07 12:55:51 +0400970func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400971 cName := r.PostFormValue("name")
972 if cName == "" {
973 http.Error(w, "no name", http.StatusBadRequest)
974 return
975 }
976 st := cluster.State{Name: cName}
977 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
978 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
979 return "", err
980 }
981 return fmt.Sprintf("create cluster: %s", cName), nil
982 }); err != nil {
983 http.Error(w, err.Error(), http.StatusInternalServerError)
984 return
985 }
986 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
987}
988
gio59946282024-10-07 12:55:51 +0400989func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400990 cName, ok := mux.Vars(r)["name"]
991 if !ok {
992 http.Error(w, "empty name", http.StatusBadRequest)
993 return
994 }
gio8c876172024-10-05 12:25:13 +0400995 tid := 0
996 if t, ok := s.tasks[cName]; ok {
997 if t.task != nil {
998 http.Error(w, "cluster task in progress", http.StatusLocked)
999 return
1000 }
1001 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001002 }
1003 m, err := s.getClusterManager(cName)
1004 if err != nil {
1005 if errors.Is(err, installer.ErrorNotFound) {
1006 http.Error(w, "not found", http.StatusNotFound)
1007 } else {
1008 http.Error(w, err.Error(), http.StatusInternalServerError)
1009 }
1010 return
1011 }
1012 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +04001013 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001014 go task.Start()
gio8c876172024-10-05 12:25:13 +04001015 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001016 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1017}
1018
gio59946282024-10-07 12:55:51 +04001019func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001020 const vpnUser = "private-network-proxy"
1021 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1022 hostname := fmt.Sprintf("cluster-%s", name)
1023 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1024 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1025 if err != nil {
1026 return installer.ReleaseResources{}, err
1027 }
1028 env, err := s.m.Config()
1029 if err != nil {
1030 return installer.ReleaseResources{}, err
1031 }
gio721c0042025-04-03 11:56:36 +04001032 keys, err := installer.NewSSHKeyPair("port-allocator")
1033 if err != nil {
1034 return installer.ReleaseResources{}, err
1035 }
1036 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1037 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1038 return installer.ReleaseResources{}, err
1039 }
1040 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1041 return installer.ReleaseResources{}, err
1042 }
giof6ad2982024-08-23 17:42:49 +04001043 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1044 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001045 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001046 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1047 "cluster": map[string]any{
1048 "name": name,
1049 "kubeconfig": kubeconfig,
1050 "ingressClassName": ingressClassName,
1051 },
1052 // TODO(gio): remove hardcoded user
1053 "vpnUser": vpnUser,
1054 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001055 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001056 })
1057 if err != nil {
1058 return installer.ReleaseResources{}, err
1059 }
1060 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1061 go s.reconciler.Reconcile(ctx)
1062 return rr, err
1063 })
1064 ch := make(chan error)
1065 t.OnDone(func(err error) {
1066 ch <- err
1067 })
1068 go t.Start()
1069 err := <-ch
1070 if err != nil {
1071 return nil, err
1072 }
1073 for {
1074 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1075 if err == nil {
1076 return ip, nil
1077 }
1078 if errors.Is(err, installer.ErrorNotFound) {
1079 time.Sleep(5 * time.Second)
1080 }
1081 }
1082 }
1083}
gio8f290322024-09-21 15:37:45 +04001084
gio59946282024-10-07 12:55:51 +04001085func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001086 return func(cm cluster.Manager) error {
1087 name := cm.State().Name
1088 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1089 app, err := installer.FindEnvApp(s.fr, "longhorn")
1090 if err != nil {
1091 return installer.ReleaseResources{}, err
1092 }
1093 env, err := s.m.Config()
1094 if err != nil {
1095 return installer.ReleaseResources{}, err
1096 }
1097 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1098 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1099 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1100 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1101 "cluster": name,
1102 })
1103 if err != nil {
1104 return installer.ReleaseResources{}, err
1105 }
1106 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1107 go s.reconciler.Reconcile(ctx)
1108 return rr, err
1109 })
1110 ch := make(chan error)
1111 t.OnDone(func(err error) {
1112 ch <- err
1113 })
1114 go t.Start()
1115 err := <-ch
1116 if err != nil {
1117 return err
1118 }
1119 cm.EnableStorage()
1120 return nil
1121 }
1122}
gio8c876172024-10-05 12:25:13 +04001123
gio59946282024-10-07 12:55:51 +04001124func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001125 return func(err error) {
1126 if err != nil {
1127 fmt.Printf("Task %s failed: %s", name, err.Error())
1128 }
1129 s.l.Lock()
1130 defer s.l.Unlock()
1131 s.tasks[name].task = nil
1132 go func() {
1133 time.Sleep(30 * time.Second)
1134 s.l.Lock()
1135 defer s.l.Unlock()
1136 if t, ok := s.tasks[name]; ok && t.id == id {
1137 delete(s.tasks, name)
1138 }
1139 }()
1140 }
1141}