blob: 3b5d5b50a005645e3ecad9dea6ce8aced3d47e6b [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"`
gio212f8002025-07-08 14:28:43 +0400176 EnvVars []installer.EnvVar `json:"envVars"`
gio218e8132025-04-22 17:11:58 +0000177}
178
179type dodoAppRendered struct {
180 Input struct {
181 Key struct {
182 Public string `json:"public"`
183 } `json:"key"`
184 } `json:"input"`
185}
186
giofc441e32024-11-11 16:26:14 +0400187func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400188 s.l.Lock()
189 defer s.l.Unlock()
giofc441e32024-11-11 16:26:14 +0400190 var req dodoAppInstallReq
191 // TODO(gio): validate that no internal fields are overridden by request
192 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
193 http.Error(w, err.Error(), http.StatusBadRequest)
194 return
195 }
196 clusters, err := s.m.GetClusters()
197 if err != nil {
198 http.Error(w, err.Error(), http.StatusInternalServerError)
199 return
200 }
201 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
202 var cfg bytes.Buffer
203 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
204 http.Error(w, err.Error(), http.StatusInternalServerError)
205 return
206 }
207 app, err := installer.NewDodoApp(cfg.Bytes())
208 if err != nil {
209 http.Error(w, err.Error(), http.StatusBadRequest)
210 return
211 }
gio218e8132025-04-22 17:11:58 +0000212 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400213 http.Error(w, err.Error(), http.StatusInternalServerError)
214 return
gioa421b062025-04-21 09:45:04 +0400215 } else {
gioda708652025-04-30 14:57:38 +0400216 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
217 if err != nil {
218 http.Error(w, err.Error(), http.StatusInternalServerError)
219 return
giof8acc612025-04-26 08:20:55 +0400220 }
gioda708652025-04-30 14:57:38 +0400221 s.im.Monitor(instanceId, outs)
gio218e8132025-04-22 17:11:58 +0000222 var cfg dodoAppRendered
223 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 }
226 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
227 Id: instanceId,
228 DeployKey: cfg.Input.Key.Public,
gio6ce44812025-05-17 07:31:54 +0400229 Access: rr.Access,
gio212f8002025-07-08 14:28:43 +0400230 EnvVars: rr.EnvVars,
gio218e8132025-04-22 17:11:58 +0000231 }); err != nil {
232 http.Error(w, err.Error(), http.StatusInternalServerError)
233 }
giofc441e32024-11-11 16:26:14 +0400234 }
235}
236
gio63a1a822025-04-23 12:59:40 +0400237func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400238 s.l.Lock()
239 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400240 instanceId, ok := mux.Vars(r)["instanceId"]
241 if !ok {
242 http.Error(w, "missing instance id", http.StatusBadRequest)
243 }
gio268787a2025-04-24 21:18:06 +0400244 if _, ok := s.tasks[instanceId]; ok {
245 http.Error(w, "task in progress", http.StatusTooEarly)
246 return
247 }
gio63a1a822025-04-23 12:59:40 +0400248 var req dodoAppInstallReq
249 // TODO(gio): validate that no internal fields are overridden by request
250 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
251 http.Error(w, err.Error(), http.StatusBadRequest)
252 return
253 }
254 clusters, err := s.m.GetClusters()
255 if err != nil {
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
259 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
260 var cfg bytes.Buffer
261 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
262 http.Error(w, err.Error(), http.StatusInternalServerError)
263 return
264 }
265 overrides := installer.CueAppData{
266 "app.cue": cfg.Bytes(),
267 }
gio268787a2025-04-24 21:18:06 +0400268 rr, err := s.m.Update(instanceId, nil, overrides)
269 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400270 http.Error(w, err.Error(), http.StatusInternalServerError)
gioda708652025-04-30 14:57:38 +0400271 return
gio63a1a822025-04-23 12:59:40 +0400272 }
gioda708652025-04-30 14:57:38 +0400273 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
274 if err != nil {
275 http.Error(w, err.Error(), http.StatusInternalServerError)
276 return
giof8acc612025-04-26 08:20:55 +0400277 }
gioda708652025-04-30 14:57:38 +0400278 s.im.Monitor(instanceId, outs)
gio268787a2025-04-24 21:18:06 +0400279 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
280 if err == nil {
281 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
282 go s.reconciler.Reconcile(ctx)
283 }
284 return rr, err
285 })
286 if _, ok := s.tasks[instanceId]; ok {
287 panic("MUST NOT REACH!")
288 }
289 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
290 t.OnDone(s.cleanTask(instanceId, 0))
291 go t.Start()
gio6ce44812025-05-17 07:31:54 +0400292 var rend dodoAppRendered
293 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&rend); err != nil {
294 http.Error(w, err.Error(), http.StatusInternalServerError)
295 }
296 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
297 Id: instanceId,
298 DeployKey: rend.Input.Key.Public,
299 Access: rr.Access,
gio212f8002025-07-08 14:28:43 +0400300 EnvVars: rr.EnvVars,
gio6ce44812025-05-17 07:31:54 +0400301 }); err != nil {
302 http.Error(w, err.Error(), http.StatusInternalServerError)
303 }
gio63a1a822025-04-23 12:59:40 +0400304}
305
gio59946282024-10-07 12:55:51 +0400306func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400307 env, err := s.m.Config()
308 if err != nil {
309 http.Error(w, err.Error(), http.StatusInternalServerError)
310 return
311 }
312 networks, err := s.m.CreateNetworks(env)
313 if err != nil {
314 http.Error(w, err.Error(), http.StatusInternalServerError)
315 return
316 }
317 if err := json.NewEncoder(w).Encode(networks); err != nil {
318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
320 }
321}
322
gio59946282024-10-07 12:55:51 +0400323func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400324 clusters, err := s.m.GetClusters()
325 if err != nil {
326 http.Error(w, err.Error(), http.StatusInternalServerError)
327 return
328 }
329 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
330 http.Error(w, err.Error(), http.StatusInternalServerError)
331 return
332 }
333}
334
335type proxyPair struct {
336 From string `json:"from"`
337 To string `json:"to"`
338}
339
gio59946282024-10-07 12:55:51 +0400340func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400341 var req proxyPair
342 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
343 http.Error(w, err.Error(), http.StatusBadRequest)
344 return
345 }
gio721c0042025-04-03 11:56:36 +0400346 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400347 http.Error(w, err.Error(), http.StatusInternalServerError)
348 return
349 }
350}
351
gio59946282024-10-07 12:55:51 +0400352func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400353 var req proxyPair
354 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
355 http.Error(w, err.Error(), http.StatusBadRequest)
356 return
357 }
gio721c0042025-04-03 11:56:36 +0400358 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
361 }
362}
363
364type app struct {
365 Name string `json:"name"`
366 Icon template.HTML `json:"icon"`
367 ShortDescription string `json:"shortDescription"`
368 Slug string `json:"slug"`
369 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
370}
371
gio59946282024-10-07 12:55:51 +0400372func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400373 all, err := s.r.GetAll()
374 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400375 http.Error(w, err.Error(), http.StatusInternalServerError)
376 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400377 }
378 resp := make([]app, len(all))
379 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400380 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400381 }
gioaa0fcdb2024-06-10 22:19:25 +0400382 w.Header().Set("Content-Type", "application/json")
383 if err := json.NewEncoder(w).Encode(resp); err != nil {
384 http.Error(w, err.Error(), http.StatusInternalServerError)
385 return
386 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400387}
388
gio59946282024-10-07 12:55:51 +0400389func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400390 slug, ok := mux.Vars(r)["slug"]
391 if !ok {
392 http.Error(w, "empty slug", http.StatusBadRequest)
393 return
394 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395 a, err := s.r.Find(slug)
396 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400397 http.Error(w, err.Error(), http.StatusInternalServerError)
398 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400399 }
gio7fbd4ad2024-08-27 10:06:39 +0400400 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400401 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400402 http.Error(w, err.Error(), http.StatusInternalServerError)
403 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400404 }
gioaa0fcdb2024-06-10 22:19:25 +0400405 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
406 w.Header().Set("Content-Type", "application/json")
407 if err := json.NewEncoder(w).Encode(resp); err != nil {
408 http.Error(w, err.Error(), http.StatusInternalServerError)
409 return
410 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400411}
412
gio59946282024-10-07 12:55:51 +0400413func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400414 slug, ok := mux.Vars(r)["slug"]
415 if !ok {
416 http.Error(w, "empty slug", http.StatusBadRequest)
417 return
418 }
gio7fbd4ad2024-08-27 10:06:39 +0400419 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400420 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400421 http.Error(w, err.Error(), http.StatusInternalServerError)
422 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400423 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400424 a, err := s.r.Find(instance.AppId)
425 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400426 http.Error(w, err.Error(), http.StatusInternalServerError)
427 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400428 }
gioaa0fcdb2024-06-10 22:19:25 +0400429 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
430 w.Header().Set("Content-Type", "application/json")
431 if err := json.NewEncoder(w).Encode(resp); err != nil {
432 http.Error(w, err.Error(), http.StatusInternalServerError)
433 return
434 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400435}
436
gio218e8132025-04-22 17:11:58 +0000437func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400438 env, err := s.m.Config()
439 if err != nil {
gio218e8132025-04-22 17:11:58 +0000440 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400441 }
442 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
443 suffix, err := suffixGen.Generate()
444 if err != nil {
gio218e8132025-04-22 17:11:58 +0000445 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400446 }
447 instanceId := app.Slug() + suffix
448 appDir := fmt.Sprintf("/apps/%s", instanceId)
449 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000450 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400451 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400452 if err == nil {
453 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
454 go s.reconciler.Reconcile(ctx)
455 }
456 return rr, err
457 })
458 if _, ok := s.tasks[instanceId]; ok {
459 panic("MUST NOT REACH!")
460 }
461 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
462 t.OnDone(s.cleanTask(instanceId, 0))
463 go t.Start()
gio218e8132025-04-22 17:11:58 +0000464 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400465}
466
gio59946282024-10-07 12:55:51 +0400467func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400468 s.l.Lock()
469 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400470 slug, ok := mux.Vars(r)["slug"]
471 if !ok {
472 http.Error(w, "empty slug", http.StatusBadRequest)
473 return
474 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400475 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400476 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400477 http.Error(w, err.Error(), http.StatusInternalServerError)
478 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400479 }
gioa421b062025-04-21 09:45:04 +0400480 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400481 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400482 http.Error(w, err.Error(), http.StatusInternalServerError)
483 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400484 }
gio218e8132025-04-22 17:11:58 +0000485 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400486 http.Error(w, err.Error(), http.StatusInternalServerError)
487 return
gioa421b062025-04-21 09:45:04 +0400488 } else {
489 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400490 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400491}
492
gio59946282024-10-07 12:55:51 +0400493func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400494 s.l.Lock()
495 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400496 slug, ok := mux.Vars(r)["slug"]
497 if !ok {
498 http.Error(w, "empty slug", http.StatusBadRequest)
499 return
500 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400501 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400502 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400503 http.Error(w, err.Error(), http.StatusInternalServerError)
504 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400505 }
gio8c876172024-10-05 12:25:13 +0400506 tid := 0
507 if t, ok := s.tasks[slug]; ok {
508 if t.task != nil {
509 http.Error(w, "Update already in progress", http.StatusBadRequest)
510 return
511 }
512 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400513 }
gio63a1a822025-04-23 12:59:40 +0400514 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400515 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400516 http.Error(w, err.Error(), http.StatusInternalServerError)
517 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400518 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400519 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
520 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400521 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400522 t.OnDone(s.cleanTask(slug, tid))
523 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400524 go t.Start()
gio268787a2025-04-24 21:18:06 +0400525 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400526 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
529 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400530}
531
gio59946282024-10-07 12:55:51 +0400532func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400533 slug, ok := mux.Vars(r)["slug"]
534 if !ok {
535 http.Error(w, "empty slug", http.StatusBadRequest)
536 return
537 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400538 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400539 http.Error(w, err.Error(), http.StatusInternalServerError)
540 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400541 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400542 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
543 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400544 if _, err := fmt.Fprint(w, "/"); err != nil {
545 http.Error(w, err.Error(), http.StatusInternalServerError)
546 return
547 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400548}
549
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400550type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400551 Apps []app
552 CurrentPage string
553 SearchTarget string
554 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400555}
556
gio59946282024-10-07 12:55:51 +0400557func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400558 pageType := mux.Vars(r)["pageType"]
559 if pageType == "" {
560 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400561 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400562 searchQuery := r.FormValue("query")
563 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400564 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400565 http.Error(w, err.Error(), http.StatusInternalServerError)
566 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400567 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400568 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400569 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400570 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400571 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400572 http.Error(w, err.Error(), http.StatusInternalServerError)
573 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400574 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400575 switch pageType {
576 case "installed":
577 if len(instances) != 0 {
578 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
579 }
580 case "not-installed":
581 if len(instances) == 0 {
582 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
583 }
584 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400585 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
586 }
587 }
588 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400589 Apps: resp,
590 CurrentPage: pageType,
591 SearchTarget: pageType,
592 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400593 }
gioaa0fcdb2024-06-10 22:19:25 +0400594 if err := s.tmpl.index.Execute(w, data); err != nil {
595 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400596 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400597}
598
599type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400600 App installer.EnvApp
601 Instance *installer.AppInstanceConfig
602 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400603 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400604 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400605 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400606}
607
gio59946282024-10-07 12:55:51 +0400608func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400609 global, err := s.m.Config()
610 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400611 http.Error(w, err.Error(), http.StatusInternalServerError)
612 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400613 }
gioaa0fcdb2024-06-10 22:19:25 +0400614 slug, ok := mux.Vars(r)["slug"]
615 if !ok {
616 http.Error(w, "empty slug", http.StatusBadRequest)
617 return
618 }
gio3cdee592024-04-17 10:15:56 +0400619 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400620 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400621 http.Error(w, err.Error(), http.StatusInternalServerError)
622 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400623 }
gio7fbd4ad2024-08-27 10:06:39 +0400624 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400625 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400626 http.Error(w, err.Error(), http.StatusInternalServerError)
627 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400628 }
giocb34ad22024-07-11 08:01:13 +0400629 networks, err := s.m.CreateNetworks(global)
630 if err != nil {
631 http.Error(w, err.Error(), http.StatusInternalServerError)
632 return
633 }
giof6ad2982024-08-23 17:42:49 +0400634 clusters, err := s.m.GetClusters()
635 if err != nil {
636 http.Error(w, err.Error(), http.StatusInternalServerError)
637 return
638 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400639 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400640 App: a,
641 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400642 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400643 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400644 CurrentPage: a.Name(),
645 }
gioaa0fcdb2024-06-10 22:19:25 +0400646 if err := s.tmpl.app.Execute(w, data); err != nil {
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400650}
651
gio59946282024-10-07 12:55:51 +0400652func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400653 s.l.Lock()
654 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400655 global, err := s.m.Config()
656 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400657 http.Error(w, err.Error(), http.StatusInternalServerError)
658 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400659 }
gioaa0fcdb2024-06-10 22:19:25 +0400660 slug, ok := mux.Vars(r)["slug"]
661 if !ok {
662 http.Error(w, "empty slug", http.StatusBadRequest)
663 return
664 }
gio8c876172024-10-05 12:25:13 +0400665 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400666 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
667 return
668 }
gio8c876172024-10-05 12:25:13 +0400669 instance, err := s.m.GetInstance(slug)
670 if err != nil {
671 http.Error(w, err.Error(), http.StatusInternalServerError)
672 return
673 }
gio63a1a822025-04-23 12:59:40 +0400674 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400675 if err != nil {
676 http.Error(w, err.Error(), http.StatusInternalServerError)
677 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400678 }
gio7fbd4ad2024-08-27 10:06:39 +0400679 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400680 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400681 http.Error(w, err.Error(), http.StatusInternalServerError)
682 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400683 }
giocb34ad22024-07-11 08:01:13 +0400684 networks, err := s.m.CreateNetworks(global)
685 if err != nil {
686 http.Error(w, err.Error(), http.StatusInternalServerError)
687 return
688 }
giof6ad2982024-08-23 17:42:49 +0400689 clusters, err := s.m.GetClusters()
690 if err != nil {
691 http.Error(w, err.Error(), http.StatusInternalServerError)
692 return
693 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400694 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400695 App: a,
gio778577f2024-04-29 09:44:38 +0400696 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400697 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400698 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400699 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400700 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400701 }
gioaa0fcdb2024-06-10 22:19:25 +0400702 if err := s.tmpl.app.Execute(w, data); err != nil {
703 http.Error(w, err.Error(), http.StatusInternalServerError)
704 return
705 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400706}
giof6ad2982024-08-23 17:42:49 +0400707
708type taskStatusData struct {
709 CurrentPage string
710 Task tasks.Task
711}
712
gio59946282024-10-07 12:55:51 +0400713func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400714 s.l.Lock()
715 defer s.l.Unlock()
716 slug, ok := mux.Vars(r)["slug"]
717 if !ok {
718 http.Error(w, "empty slug", http.StatusBadRequest)
719 return
720 }
721 t, ok := s.tasks[slug]
722 if !ok {
723 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400724 return
725 }
gio8c876172024-10-05 12:25:13 +0400726 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400727 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
728 return
729 }
730 data := taskStatusData{
731 CurrentPage: "",
732 Task: t.task,
733 }
734 if err := s.tmpl.task.Execute(w, data); err != nil {
735 http.Error(w, err.Error(), http.StatusInternalServerError)
736 return
737 }
738}
739
gio268787a2025-04-24 21:18:06 +0400740type resourceStatus struct {
741 Type string `json:"type"`
742 Name string `json:"name"`
743 Status string `json:"status"`
744}
745
746func extractResources(t tasks.Task) []resourceStatus {
747 var ret []resourceStatus
748 if t.Resource() != nil {
749 ret = append(ret, resourceStatus{
750 Type: t.Resource().Type,
751 Name: t.Resource().Name,
752 Status: tasks.StatusString(t.Status()),
753 })
754 }
755 for _, st := range t.Subtasks() {
756 ret = append(ret, extractResources(st)...)
757 }
758 return ret
759}
760
giof8acc612025-04-26 08:20:55 +0400761func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400762 s.l.Lock()
763 defer s.l.Unlock()
764 instanceId, ok := mux.Vars(r)["instanceId"]
765 if !ok {
766 http.Error(w, "empty slug", http.StatusBadRequest)
767 return
768 }
giof8acc612025-04-26 08:20:55 +0400769 statuses, err := s.im.Get(instanceId)
770 if err != nil {
771 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400772 return
773 }
gioda708652025-04-30 14:57:38 +0400774 ret := []resourceStatus{}
775 for r, s := range statuses {
776 ret = append(ret, resourceStatus{
777 Type: r.Type,
778 Name: r.Name,
779 Status: status.StatusString(s),
giof8acc612025-04-26 08:20:55 +0400780 })
781 }
gioda708652025-04-30 14:57:38 +0400782 json.NewEncoder(w).Encode(ret)
gio268787a2025-04-24 21:18:06 +0400783}
784
giof6ad2982024-08-23 17:42:49 +0400785type clustersData struct {
786 CurrentPage string
787 Clusters []cluster.State
788}
789
gio59946282024-10-07 12:55:51 +0400790func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400791 clusters, err := s.m.GetClusters()
792 if err != nil {
793 http.Error(w, err.Error(), http.StatusInternalServerError)
794 return
795 }
796 data := clustersData{
797 "clusters",
798 clusters,
799 }
800 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
801 http.Error(w, err.Error(), http.StatusInternalServerError)
802 return
803 }
804}
805
806type clusterData struct {
807 CurrentPage string
808 Cluster cluster.State
809}
810
gio59946282024-10-07 12:55:51 +0400811func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400812 name, ok := mux.Vars(r)["name"]
813 if !ok {
814 http.Error(w, "empty name", http.StatusBadRequest)
815 return
816 }
817 m, err := s.getClusterManager(name)
818 if err != nil {
819 if errors.Is(err, installer.ErrorNotFound) {
820 http.Error(w, "not found", http.StatusNotFound)
821 } else {
822 http.Error(w, err.Error(), http.StatusInternalServerError)
823 }
824 return
825 }
826 data := clusterData{
827 "clusters",
828 m.State(),
829 }
830 if err := s.tmpl.cluster.Execute(w, data); err != nil {
831 http.Error(w, err.Error(), http.StatusInternalServerError)
832 return
833 }
834}
835
gio59946282024-10-07 12:55:51 +0400836func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400837 cName, ok := mux.Vars(r)["name"]
838 if !ok {
839 http.Error(w, "empty name", http.StatusBadRequest)
840 return
841 }
gio8c876172024-10-05 12:25:13 +0400842 tid := 0
843 if t, ok := s.tasks[cName]; ok {
844 if t.task != nil {
845 http.Error(w, "cluster task in progress", http.StatusLocked)
846 return
847 }
848 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400849 }
850 m, err := s.getClusterManager(cName)
851 if err != nil {
852 if errors.Is(err, installer.ErrorNotFound) {
853 http.Error(w, "not found", http.StatusNotFound)
854 } else {
855 http.Error(w, err.Error(), http.StatusInternalServerError)
856 }
857 return
858 }
859 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400860 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400861 go task.Start()
gio8c876172024-10-05 12:25:13 +0400862 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400863 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
864}
865
gio59946282024-10-07 12:55:51 +0400866func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400867 s.l.Lock()
868 defer s.l.Unlock()
869 cName, ok := mux.Vars(r)["cluster"]
870 if !ok {
871 http.Error(w, "empty name", http.StatusBadRequest)
872 return
873 }
gio8c876172024-10-05 12:25:13 +0400874 tid := 0
875 if t, ok := s.tasks[cName]; ok {
876 if t.task != nil {
877 http.Error(w, "cluster task in progress", http.StatusLocked)
878 return
879 }
880 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400881 }
882 sName, ok := mux.Vars(r)["server"]
883 if !ok {
884 http.Error(w, "empty name", http.StatusBadRequest)
885 return
886 }
887 m, err := s.getClusterManager(cName)
888 if err != nil {
889 if errors.Is(err, installer.ErrorNotFound) {
890 http.Error(w, "not found", http.StatusNotFound)
891 } else {
892 http.Error(w, err.Error(), http.StatusInternalServerError)
893 }
894 return
895 }
896 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400897 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400898 go task.Start()
gio8c876172024-10-05 12:25:13 +0400899 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400900 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
901}
902
gio59946282024-10-07 12:55:51 +0400903func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400904 clusters, err := s.m.GetClusters()
905 if err != nil {
906 return nil, err
907 }
908 var c *cluster.State
909 for _, i := range clusters {
910 if i.Name == cName {
911 c = &i
912 break
913 }
914 }
915 if c == nil {
916 return nil, installer.ErrorNotFound
917 }
918 return cluster.RestoreKubeManager(*c)
919}
920
gio59946282024-10-07 12:55:51 +0400921func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400922 s.l.Lock()
923 defer s.l.Unlock()
924 cName, ok := mux.Vars(r)["cluster"]
925 if !ok {
926 http.Error(w, "empty name", http.StatusBadRequest)
927 return
928 }
gio8c876172024-10-05 12:25:13 +0400929 tid := 0
930 if t, ok := s.tasks[cName]; ok {
931 if t.task != nil {
932 http.Error(w, "cluster task in progress", http.StatusLocked)
933 return
934 }
935 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400936 }
937 m, err := s.getClusterManager(cName)
938 if err != nil {
939 if errors.Is(err, installer.ErrorNotFound) {
940 http.Error(w, "not found", http.StatusNotFound)
941 } else {
942 http.Error(w, err.Error(), http.StatusInternalServerError)
943 }
944 return
945 }
946 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400947 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400948 if ip == nil {
949 http.Error(w, "invalid ip", http.StatusBadRequest)
950 return
951 }
952 port := 22
953 if p := r.PostFormValue("port"); p != "" {
954 port, err = strconv.Atoi(p)
955 if err != nil {
956 http.Error(w, err.Error(), http.StatusBadRequest)
957 return
958 }
959 }
960 server := cluster.Server{
961 IP: ip,
962 Port: port,
963 User: r.PostFormValue("user"),
964 Password: r.PostFormValue("password"),
965 }
966 var task tasks.Task
967 switch strings.ToLower(t) {
968 case "controller":
969 if len(m.State().Controllers) == 0 {
970 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
971 } else {
972 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
973 }
974 case "worker":
975 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
976 default:
977 http.Error(w, "invalid type", http.StatusBadRequest)
978 return
979 }
gio8c876172024-10-05 12:25:13 +0400980 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400981 go task.Start()
gio8c876172024-10-05 12:25:13 +0400982 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400983 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
984}
985
gio59946282024-10-07 12:55:51 +0400986func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400987 cName := r.PostFormValue("name")
988 if cName == "" {
989 http.Error(w, "no name", http.StatusBadRequest)
990 return
991 }
992 st := cluster.State{Name: cName}
993 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
994 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
995 return "", err
996 }
997 return fmt.Sprintf("create cluster: %s", cName), nil
998 }); err != nil {
999 http.Error(w, err.Error(), http.StatusInternalServerError)
1000 return
1001 }
1002 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
1003}
1004
gio59946282024-10-07 12:55:51 +04001005func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001006 cName, ok := mux.Vars(r)["name"]
1007 if !ok {
1008 http.Error(w, "empty name", http.StatusBadRequest)
1009 return
1010 }
gio8c876172024-10-05 12:25:13 +04001011 tid := 0
1012 if t, ok := s.tasks[cName]; ok {
1013 if t.task != nil {
1014 http.Error(w, "cluster task in progress", http.StatusLocked)
1015 return
1016 }
1017 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001018 }
1019 m, err := s.getClusterManager(cName)
1020 if err != nil {
1021 if errors.Is(err, installer.ErrorNotFound) {
1022 http.Error(w, "not found", http.StatusNotFound)
1023 } else {
1024 http.Error(w, err.Error(), http.StatusInternalServerError)
1025 }
1026 return
1027 }
1028 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +04001029 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001030 go task.Start()
gio8c876172024-10-05 12:25:13 +04001031 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001032 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1033}
1034
gio59946282024-10-07 12:55:51 +04001035func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001036 const vpnUser = "private-network-proxy"
1037 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1038 hostname := fmt.Sprintf("cluster-%s", name)
1039 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1040 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1041 if err != nil {
1042 return installer.ReleaseResources{}, err
1043 }
1044 env, err := s.m.Config()
1045 if err != nil {
1046 return installer.ReleaseResources{}, err
1047 }
gio721c0042025-04-03 11:56:36 +04001048 keys, err := installer.NewSSHKeyPair("port-allocator")
1049 if err != nil {
1050 return installer.ReleaseResources{}, err
1051 }
1052 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1053 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1054 return installer.ReleaseResources{}, err
1055 }
1056 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1057 return installer.ReleaseResources{}, err
1058 }
giof6ad2982024-08-23 17:42:49 +04001059 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1060 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001061 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001062 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1063 "cluster": map[string]any{
1064 "name": name,
1065 "kubeconfig": kubeconfig,
1066 "ingressClassName": ingressClassName,
1067 },
1068 // TODO(gio): remove hardcoded user
1069 "vpnUser": vpnUser,
1070 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001071 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001072 })
1073 if err != nil {
1074 return installer.ReleaseResources{}, err
1075 }
1076 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1077 go s.reconciler.Reconcile(ctx)
1078 return rr, err
1079 })
1080 ch := make(chan error)
1081 t.OnDone(func(err error) {
1082 ch <- err
1083 })
1084 go t.Start()
1085 err := <-ch
1086 if err != nil {
1087 return nil, err
1088 }
1089 for {
1090 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1091 if err == nil {
1092 return ip, nil
1093 }
1094 if errors.Is(err, installer.ErrorNotFound) {
1095 time.Sleep(5 * time.Second)
1096 }
1097 }
1098 }
1099}
gio8f290322024-09-21 15:37:45 +04001100
gio59946282024-10-07 12:55:51 +04001101func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001102 return func(cm cluster.Manager) error {
1103 name := cm.State().Name
1104 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1105 app, err := installer.FindEnvApp(s.fr, "longhorn")
1106 if err != nil {
1107 return installer.ReleaseResources{}, err
1108 }
1109 env, err := s.m.Config()
1110 if err != nil {
1111 return installer.ReleaseResources{}, err
1112 }
1113 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1114 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1115 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1116 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1117 "cluster": name,
1118 })
1119 if err != nil {
1120 return installer.ReleaseResources{}, err
1121 }
1122 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1123 go s.reconciler.Reconcile(ctx)
1124 return rr, err
1125 })
1126 ch := make(chan error)
1127 t.OnDone(func(err error) {
1128 ch <- err
1129 })
1130 go t.Start()
1131 err := <-ch
1132 if err != nil {
1133 return err
1134 }
1135 cm.EnableStorage()
1136 return nil
1137 }
1138}
gio8c876172024-10-05 12:25:13 +04001139
gio59946282024-10-07 12:55:51 +04001140func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001141 return func(err error) {
1142 if err != nil {
1143 fmt.Printf("Task %s failed: %s", name, err.Error())
1144 }
1145 s.l.Lock()
1146 defer s.l.Unlock()
1147 s.tasks[name].task = nil
1148 go func() {
1149 time.Sleep(30 * time.Second)
1150 s.l.Lock()
1151 defer s.l.Unlock()
1152 if t, ok := s.tasks[name]; ok && t.id == id {
1153 delete(s.tasks, name)
1154 }
1155 }()
1156 }
1157}