blob: c3bd2368983e052613265ac0a73ff845852ce736 [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()
gioe10ba162025-07-31 19:52:29 +0400190 fmt.Println("INSTALLING DODO APP")
191 defer func() {
192 fmt.Println("DONE")
193 }()
giofc441e32024-11-11 16:26:14 +0400194 var req dodoAppInstallReq
195 // TODO(gio): validate that no internal fields are overridden by request
196 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
197 http.Error(w, err.Error(), http.StatusBadRequest)
198 return
199 }
200 clusters, err := s.m.GetClusters()
201 if err != nil {
202 http.Error(w, err.Error(), http.StatusInternalServerError)
203 return
204 }
205 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
206 var cfg bytes.Buffer
207 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 return
210 }
211 app, err := installer.NewDodoApp(cfg.Bytes())
212 if err != nil {
213 http.Error(w, err.Error(), http.StatusBadRequest)
214 return
215 }
gio218e8132025-04-22 17:11:58 +0000216 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400217 http.Error(w, err.Error(), http.StatusInternalServerError)
218 return
gioa421b062025-04-21 09:45:04 +0400219 } else {
gioda708652025-04-30 14:57:38 +0400220 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
221 if err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
giof8acc612025-04-26 08:20:55 +0400224 }
gioda708652025-04-30 14:57:38 +0400225 s.im.Monitor(instanceId, outs)
gio218e8132025-04-22 17:11:58 +0000226 var cfg dodoAppRendered
227 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 }
230 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
231 Id: instanceId,
232 DeployKey: cfg.Input.Key.Public,
gio6ce44812025-05-17 07:31:54 +0400233 Access: rr.Access,
gio212f8002025-07-08 14:28:43 +0400234 EnvVars: rr.EnvVars,
gio218e8132025-04-22 17:11:58 +0000235 }); err != nil {
236 http.Error(w, err.Error(), http.StatusInternalServerError)
237 }
giofc441e32024-11-11 16:26:14 +0400238 }
239}
240
gio63a1a822025-04-23 12:59:40 +0400241func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400242 s.l.Lock()
243 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400244 instanceId, ok := mux.Vars(r)["instanceId"]
245 if !ok {
246 http.Error(w, "missing instance id", http.StatusBadRequest)
247 }
gio268787a2025-04-24 21:18:06 +0400248 if _, ok := s.tasks[instanceId]; ok {
249 http.Error(w, "task in progress", http.StatusTooEarly)
250 return
251 }
gio63a1a822025-04-23 12:59:40 +0400252 var req dodoAppInstallReq
253 // TODO(gio): validate that no internal fields are overridden by request
254 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
255 http.Error(w, err.Error(), http.StatusBadRequest)
256 return
257 }
258 clusters, err := s.m.GetClusters()
259 if err != nil {
260 http.Error(w, err.Error(), http.StatusInternalServerError)
261 return
262 }
263 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
264 var cfg bytes.Buffer
265 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
266 http.Error(w, err.Error(), http.StatusInternalServerError)
267 return
268 }
269 overrides := installer.CueAppData{
270 "app.cue": cfg.Bytes(),
271 }
gio268787a2025-04-24 21:18:06 +0400272 rr, err := s.m.Update(instanceId, nil, overrides)
273 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400274 http.Error(w, err.Error(), http.StatusInternalServerError)
gioda708652025-04-30 14:57:38 +0400275 return
gio63a1a822025-04-23 12:59:40 +0400276 }
gioda708652025-04-30 14:57:38 +0400277 outs, err := status.DecodeResourceOuts(rr.RenderedRaw)
278 if err != nil {
279 http.Error(w, err.Error(), http.StatusInternalServerError)
280 return
giof8acc612025-04-26 08:20:55 +0400281 }
gioda708652025-04-30 14:57:38 +0400282 s.im.Monitor(instanceId, outs)
gio268787a2025-04-24 21:18:06 +0400283 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
284 if err == nil {
285 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
286 go s.reconciler.Reconcile(ctx)
287 }
288 return rr, err
289 })
290 if _, ok := s.tasks[instanceId]; ok {
291 panic("MUST NOT REACH!")
292 }
293 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
294 t.OnDone(s.cleanTask(instanceId, 0))
295 go t.Start()
gio6ce44812025-05-17 07:31:54 +0400296 var rend dodoAppRendered
297 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&rend); err != nil {
298 http.Error(w, err.Error(), http.StatusInternalServerError)
299 }
300 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
301 Id: instanceId,
302 DeployKey: rend.Input.Key.Public,
303 Access: rr.Access,
gio212f8002025-07-08 14:28:43 +0400304 EnvVars: rr.EnvVars,
gio6ce44812025-05-17 07:31:54 +0400305 }); err != nil {
306 http.Error(w, err.Error(), http.StatusInternalServerError)
307 }
gio63a1a822025-04-23 12:59:40 +0400308}
309
gio59946282024-10-07 12:55:51 +0400310func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400311 env, err := s.m.Config()
312 if err != nil {
313 http.Error(w, err.Error(), http.StatusInternalServerError)
314 return
315 }
316 networks, err := s.m.CreateNetworks(env)
317 if err != nil {
318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
320 }
321 if err := json.NewEncoder(w).Encode(networks); err != nil {
322 http.Error(w, err.Error(), http.StatusInternalServerError)
323 return
324 }
325}
326
gio59946282024-10-07 12:55:51 +0400327func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400328 clusters, err := s.m.GetClusters()
329 if err != nil {
330 http.Error(w, err.Error(), http.StatusInternalServerError)
331 return
332 }
333 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
334 http.Error(w, err.Error(), http.StatusInternalServerError)
335 return
336 }
337}
338
339type proxyPair struct {
340 From string `json:"from"`
341 To string `json:"to"`
342}
343
gio59946282024-10-07 12:55:51 +0400344func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400345 var req proxyPair
346 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
347 http.Error(w, err.Error(), http.StatusBadRequest)
348 return
349 }
gio721c0042025-04-03 11:56:36 +0400350 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400351 http.Error(w, err.Error(), http.StatusInternalServerError)
352 return
353 }
354}
355
gio59946282024-10-07 12:55:51 +0400356func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400357 var req proxyPair
358 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
359 http.Error(w, err.Error(), http.StatusBadRequest)
360 return
361 }
gio721c0042025-04-03 11:56:36 +0400362 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400363 http.Error(w, err.Error(), http.StatusInternalServerError)
364 return
365 }
366}
367
368type app struct {
369 Name string `json:"name"`
370 Icon template.HTML `json:"icon"`
371 ShortDescription string `json:"shortDescription"`
372 Slug string `json:"slug"`
373 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
374}
375
gio59946282024-10-07 12:55:51 +0400376func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400377 all, err := s.r.GetAll()
378 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400379 http.Error(w, err.Error(), http.StatusInternalServerError)
380 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400381 }
382 resp := make([]app, len(all))
383 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400384 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400385 }
gioaa0fcdb2024-06-10 22:19:25 +0400386 w.Header().Set("Content-Type", "application/json")
387 if err := json.NewEncoder(w).Encode(resp); err != nil {
388 http.Error(w, err.Error(), http.StatusInternalServerError)
389 return
390 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400391}
392
gio59946282024-10-07 12:55:51 +0400393func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400394 slug, ok := mux.Vars(r)["slug"]
395 if !ok {
396 http.Error(w, "empty slug", http.StatusBadRequest)
397 return
398 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400399 a, err := s.r.Find(slug)
400 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400401 http.Error(w, err.Error(), http.StatusInternalServerError)
402 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400403 }
gio7fbd4ad2024-08-27 10:06:39 +0400404 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400405 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400406 http.Error(w, err.Error(), http.StatusInternalServerError)
407 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400408 }
gioaa0fcdb2024-06-10 22:19:25 +0400409 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
410 w.Header().Set("Content-Type", "application/json")
411 if err := json.NewEncoder(w).Encode(resp); err != nil {
412 http.Error(w, err.Error(), http.StatusInternalServerError)
413 return
414 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400415}
416
gio59946282024-10-07 12:55:51 +0400417func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400418 slug, ok := mux.Vars(r)["slug"]
419 if !ok {
420 http.Error(w, "empty slug", http.StatusBadRequest)
421 return
422 }
gio7fbd4ad2024-08-27 10:06:39 +0400423 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400424 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400425 http.Error(w, err.Error(), http.StatusInternalServerError)
426 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400427 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400428 a, err := s.r.Find(instance.AppId)
429 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400430 http.Error(w, err.Error(), http.StatusInternalServerError)
431 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400432 }
gioaa0fcdb2024-06-10 22:19:25 +0400433 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
434 w.Header().Set("Content-Type", "application/json")
435 if err := json.NewEncoder(w).Encode(resp); err != nil {
436 http.Error(w, err.Error(), http.StatusInternalServerError)
437 return
438 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400439}
440
gio218e8132025-04-22 17:11:58 +0000441func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400442 env, err := s.m.Config()
443 if err != nil {
gio218e8132025-04-22 17:11:58 +0000444 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400445 }
446 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
447 suffix, err := suffixGen.Generate()
448 if err != nil {
gio218e8132025-04-22 17:11:58 +0000449 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400450 }
451 instanceId := app.Slug() + suffix
452 appDir := fmt.Sprintf("/apps/%s", instanceId)
453 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000454 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400455 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400456 if err == nil {
457 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
458 go s.reconciler.Reconcile(ctx)
459 }
460 return rr, err
461 })
462 if _, ok := s.tasks[instanceId]; ok {
463 panic("MUST NOT REACH!")
464 }
465 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
466 t.OnDone(s.cleanTask(instanceId, 0))
467 go t.Start()
gio218e8132025-04-22 17:11:58 +0000468 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400469}
470
gio59946282024-10-07 12:55:51 +0400471func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400472 s.l.Lock()
473 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400474 slug, ok := mux.Vars(r)["slug"]
475 if !ok {
476 http.Error(w, "empty slug", http.StatusBadRequest)
477 return
478 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400479 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400480 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400481 http.Error(w, err.Error(), http.StatusInternalServerError)
482 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400483 }
gioa421b062025-04-21 09:45:04 +0400484 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400485 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400486 http.Error(w, err.Error(), http.StatusInternalServerError)
487 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400488 }
gio218e8132025-04-22 17:11:58 +0000489 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400490 http.Error(w, err.Error(), http.StatusInternalServerError)
491 return
gioa421b062025-04-21 09:45:04 +0400492 } else {
493 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400494 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400495}
496
gio59946282024-10-07 12:55:51 +0400497func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400498 s.l.Lock()
499 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400500 slug, ok := mux.Vars(r)["slug"]
501 if !ok {
502 http.Error(w, "empty slug", http.StatusBadRequest)
503 return
504 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400505 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400506 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400507 http.Error(w, err.Error(), http.StatusInternalServerError)
508 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400509 }
gio8c876172024-10-05 12:25:13 +0400510 tid := 0
511 if t, ok := s.tasks[slug]; ok {
512 if t.task != nil {
513 http.Error(w, "Update already in progress", http.StatusBadRequest)
514 return
515 }
516 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400517 }
gio63a1a822025-04-23 12:59:40 +0400518 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400519 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400520 http.Error(w, err.Error(), http.StatusInternalServerError)
521 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400522 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400523 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
524 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400525 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400526 t.OnDone(s.cleanTask(slug, tid))
527 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400528 go t.Start()
gio268787a2025-04-24 21:18:06 +0400529 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400530 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400531 http.Error(w, err.Error(), http.StatusInternalServerError)
532 return
533 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400534}
535
gio59946282024-10-07 12:55:51 +0400536func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400537 slug, ok := mux.Vars(r)["slug"]
538 if !ok {
539 http.Error(w, "empty slug", http.StatusBadRequest)
540 return
541 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400542 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400543 http.Error(w, err.Error(), http.StatusInternalServerError)
544 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400545 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400546 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
547 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400548 if _, err := fmt.Fprint(w, "/"); err != nil {
549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
551 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552}
553
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400554type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400555 Apps []app
556 CurrentPage string
557 SearchTarget string
558 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400559}
560
gio59946282024-10-07 12:55:51 +0400561func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400562 pageType := mux.Vars(r)["pageType"]
563 if pageType == "" {
564 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400565 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400566 searchQuery := r.FormValue("query")
567 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400568 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400572 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400573 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400574 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400575 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400576 http.Error(w, err.Error(), http.StatusInternalServerError)
577 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400578 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400579 switch pageType {
580 case "installed":
581 if len(instances) != 0 {
582 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
583 }
584 case "not-installed":
585 if len(instances) == 0 {
586 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
587 }
588 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400589 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
590 }
591 }
592 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400593 Apps: resp,
594 CurrentPage: pageType,
595 SearchTarget: pageType,
596 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400597 }
gioaa0fcdb2024-06-10 22:19:25 +0400598 if err := s.tmpl.index.Execute(w, data); err != nil {
599 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400600 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400601}
602
603type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400604 App installer.EnvApp
605 Instance *installer.AppInstanceConfig
606 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400607 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400608 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400609 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400610}
611
gio59946282024-10-07 12:55:51 +0400612func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400613 global, err := s.m.Config()
614 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400617 }
gioaa0fcdb2024-06-10 22:19:25 +0400618 slug, ok := mux.Vars(r)["slug"]
619 if !ok {
620 http.Error(w, "empty slug", http.StatusBadRequest)
621 return
622 }
gio3cdee592024-04-17 10:15:56 +0400623 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400624 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400625 http.Error(w, err.Error(), http.StatusInternalServerError)
626 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400627 }
gio7fbd4ad2024-08-27 10:06:39 +0400628 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400629 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400630 http.Error(w, err.Error(), http.StatusInternalServerError)
631 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400632 }
giocb34ad22024-07-11 08:01:13 +0400633 networks, err := s.m.CreateNetworks(global)
634 if err != nil {
635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
637 }
giof6ad2982024-08-23 17:42:49 +0400638 clusters, err := s.m.GetClusters()
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400643 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400644 App: a,
645 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400646 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400647 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400648 CurrentPage: a.Name(),
649 }
gioaa0fcdb2024-06-10 22:19:25 +0400650 if err := s.tmpl.app.Execute(w, data); err != nil {
651 http.Error(w, err.Error(), http.StatusInternalServerError)
652 return
653 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400654}
655
gio59946282024-10-07 12:55:51 +0400656func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400657 s.l.Lock()
658 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400659 global, err := s.m.Config()
660 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400661 http.Error(w, err.Error(), http.StatusInternalServerError)
662 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400663 }
gioaa0fcdb2024-06-10 22:19:25 +0400664 slug, ok := mux.Vars(r)["slug"]
665 if !ok {
666 http.Error(w, "empty slug", http.StatusBadRequest)
667 return
668 }
gio8c876172024-10-05 12:25:13 +0400669 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400670 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
671 return
672 }
gio8c876172024-10-05 12:25:13 +0400673 instance, err := s.m.GetInstance(slug)
674 if err != nil {
675 http.Error(w, err.Error(), http.StatusInternalServerError)
676 return
677 }
gio63a1a822025-04-23 12:59:40 +0400678 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400679 if err != nil {
680 http.Error(w, err.Error(), http.StatusInternalServerError)
681 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400682 }
gio7fbd4ad2024-08-27 10:06:39 +0400683 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400684 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400685 http.Error(w, err.Error(), http.StatusInternalServerError)
686 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400687 }
giocb34ad22024-07-11 08:01:13 +0400688 networks, err := s.m.CreateNetworks(global)
689 if err != nil {
690 http.Error(w, err.Error(), http.StatusInternalServerError)
691 return
692 }
giof6ad2982024-08-23 17:42:49 +0400693 clusters, err := s.m.GetClusters()
694 if err != nil {
695 http.Error(w, err.Error(), http.StatusInternalServerError)
696 return
697 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400698 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400699 App: a,
gio778577f2024-04-29 09:44:38 +0400700 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400701 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400702 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400703 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400704 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400705 }
gioaa0fcdb2024-06-10 22:19:25 +0400706 if err := s.tmpl.app.Execute(w, data); err != nil {
707 http.Error(w, err.Error(), http.StatusInternalServerError)
708 return
709 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400710}
giof6ad2982024-08-23 17:42:49 +0400711
712type taskStatusData struct {
713 CurrentPage string
714 Task tasks.Task
715}
716
gio59946282024-10-07 12:55:51 +0400717func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400718 s.l.Lock()
719 defer s.l.Unlock()
720 slug, ok := mux.Vars(r)["slug"]
721 if !ok {
722 http.Error(w, "empty slug", http.StatusBadRequest)
723 return
724 }
725 t, ok := s.tasks[slug]
726 if !ok {
727 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400728 return
729 }
gio8c876172024-10-05 12:25:13 +0400730 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400731 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
732 return
733 }
734 data := taskStatusData{
735 CurrentPage: "",
736 Task: t.task,
737 }
738 if err := s.tmpl.task.Execute(w, data); err != nil {
739 http.Error(w, err.Error(), http.StatusInternalServerError)
740 return
741 }
742}
743
gio268787a2025-04-24 21:18:06 +0400744type resourceStatus struct {
745 Type string `json:"type"`
746 Name string `json:"name"`
747 Status string `json:"status"`
748}
749
750func extractResources(t tasks.Task) []resourceStatus {
751 var ret []resourceStatus
752 if t.Resource() != nil {
753 ret = append(ret, resourceStatus{
754 Type: t.Resource().Type,
755 Name: t.Resource().Name,
756 Status: tasks.StatusString(t.Status()),
757 })
758 }
759 for _, st := range t.Subtasks() {
760 ret = append(ret, extractResources(st)...)
761 }
762 return ret
763}
764
giof8acc612025-04-26 08:20:55 +0400765func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400766 s.l.Lock()
767 defer s.l.Unlock()
768 instanceId, ok := mux.Vars(r)["instanceId"]
769 if !ok {
770 http.Error(w, "empty slug", http.StatusBadRequest)
771 return
772 }
giof8acc612025-04-26 08:20:55 +0400773 statuses, err := s.im.Get(instanceId)
774 if err != nil {
775 http.Error(w, err.Error(), http.StatusInternalServerError)
gio268787a2025-04-24 21:18:06 +0400776 return
777 }
gioda708652025-04-30 14:57:38 +0400778 ret := []resourceStatus{}
779 for r, s := range statuses {
780 ret = append(ret, resourceStatus{
781 Type: r.Type,
782 Name: r.Name,
783 Status: status.StatusString(s),
giof8acc612025-04-26 08:20:55 +0400784 })
785 }
gioda708652025-04-30 14:57:38 +0400786 json.NewEncoder(w).Encode(ret)
gio268787a2025-04-24 21:18:06 +0400787}
788
giof6ad2982024-08-23 17:42:49 +0400789type clustersData struct {
790 CurrentPage string
791 Clusters []cluster.State
792}
793
gio59946282024-10-07 12:55:51 +0400794func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400795 clusters, err := s.m.GetClusters()
796 if err != nil {
797 http.Error(w, err.Error(), http.StatusInternalServerError)
798 return
799 }
800 data := clustersData{
801 "clusters",
802 clusters,
803 }
804 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
805 http.Error(w, err.Error(), http.StatusInternalServerError)
806 return
807 }
808}
809
810type clusterData struct {
811 CurrentPage string
812 Cluster cluster.State
813}
814
gio59946282024-10-07 12:55:51 +0400815func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400816 name, ok := mux.Vars(r)["name"]
817 if !ok {
818 http.Error(w, "empty name", http.StatusBadRequest)
819 return
820 }
821 m, err := s.getClusterManager(name)
822 if err != nil {
823 if errors.Is(err, installer.ErrorNotFound) {
824 http.Error(w, "not found", http.StatusNotFound)
825 } else {
826 http.Error(w, err.Error(), http.StatusInternalServerError)
827 }
828 return
829 }
830 data := clusterData{
831 "clusters",
832 m.State(),
833 }
834 if err := s.tmpl.cluster.Execute(w, data); err != nil {
835 http.Error(w, err.Error(), http.StatusInternalServerError)
836 return
837 }
838}
839
gio59946282024-10-07 12:55:51 +0400840func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400841 cName, ok := mux.Vars(r)["name"]
842 if !ok {
843 http.Error(w, "empty name", http.StatusBadRequest)
844 return
845 }
gio8c876172024-10-05 12:25:13 +0400846 tid := 0
847 if t, ok := s.tasks[cName]; ok {
848 if t.task != nil {
849 http.Error(w, "cluster task in progress", http.StatusLocked)
850 return
851 }
852 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400853 }
854 m, err := s.getClusterManager(cName)
855 if err != nil {
856 if errors.Is(err, installer.ErrorNotFound) {
857 http.Error(w, "not found", http.StatusNotFound)
858 } else {
859 http.Error(w, err.Error(), http.StatusInternalServerError)
860 }
861 return
862 }
863 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400864 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400865 go task.Start()
gio8c876172024-10-05 12:25:13 +0400866 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400867 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
868}
869
gio59946282024-10-07 12:55:51 +0400870func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400871 s.l.Lock()
872 defer s.l.Unlock()
873 cName, ok := mux.Vars(r)["cluster"]
874 if !ok {
875 http.Error(w, "empty name", http.StatusBadRequest)
876 return
877 }
gio8c876172024-10-05 12:25:13 +0400878 tid := 0
879 if t, ok := s.tasks[cName]; ok {
880 if t.task != nil {
881 http.Error(w, "cluster task in progress", http.StatusLocked)
882 return
883 }
884 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400885 }
886 sName, ok := mux.Vars(r)["server"]
887 if !ok {
888 http.Error(w, "empty name", http.StatusBadRequest)
889 return
890 }
891 m, err := s.getClusterManager(cName)
892 if err != nil {
893 if errors.Is(err, installer.ErrorNotFound) {
894 http.Error(w, "not found", http.StatusNotFound)
895 } else {
896 http.Error(w, err.Error(), http.StatusInternalServerError)
897 }
898 return
899 }
900 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400901 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400902 go task.Start()
gio8c876172024-10-05 12:25:13 +0400903 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400904 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
905}
906
gio59946282024-10-07 12:55:51 +0400907func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400908 clusters, err := s.m.GetClusters()
909 if err != nil {
910 return nil, err
911 }
912 var c *cluster.State
913 for _, i := range clusters {
914 if i.Name == cName {
915 c = &i
916 break
917 }
918 }
919 if c == nil {
920 return nil, installer.ErrorNotFound
921 }
922 return cluster.RestoreKubeManager(*c)
923}
924
gio59946282024-10-07 12:55:51 +0400925func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400926 s.l.Lock()
927 defer s.l.Unlock()
928 cName, ok := mux.Vars(r)["cluster"]
929 if !ok {
930 http.Error(w, "empty name", http.StatusBadRequest)
931 return
932 }
gio8c876172024-10-05 12:25:13 +0400933 tid := 0
934 if t, ok := s.tasks[cName]; ok {
935 if t.task != nil {
936 http.Error(w, "cluster task in progress", http.StatusLocked)
937 return
938 }
939 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400940 }
941 m, err := s.getClusterManager(cName)
942 if err != nil {
943 if errors.Is(err, installer.ErrorNotFound) {
944 http.Error(w, "not found", http.StatusNotFound)
945 } else {
946 http.Error(w, err.Error(), http.StatusInternalServerError)
947 }
948 return
949 }
950 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400951 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400952 if ip == nil {
953 http.Error(w, "invalid ip", http.StatusBadRequest)
954 return
955 }
956 port := 22
957 if p := r.PostFormValue("port"); p != "" {
958 port, err = strconv.Atoi(p)
959 if err != nil {
960 http.Error(w, err.Error(), http.StatusBadRequest)
961 return
962 }
963 }
964 server := cluster.Server{
965 IP: ip,
966 Port: port,
967 User: r.PostFormValue("user"),
968 Password: r.PostFormValue("password"),
969 }
970 var task tasks.Task
971 switch strings.ToLower(t) {
972 case "controller":
973 if len(m.State().Controllers) == 0 {
974 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
975 } else {
976 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
977 }
978 case "worker":
979 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
980 default:
981 http.Error(w, "invalid type", http.StatusBadRequest)
982 return
983 }
gio8c876172024-10-05 12:25:13 +0400984 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400985 go task.Start()
gio8c876172024-10-05 12:25:13 +0400986 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400987 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
988}
989
gio59946282024-10-07 12:55:51 +0400990func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400991 cName := r.PostFormValue("name")
992 if cName == "" {
993 http.Error(w, "no name", http.StatusBadRequest)
994 return
995 }
996 st := cluster.State{Name: cName}
997 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
998 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
999 return "", err
1000 }
1001 return fmt.Sprintf("create cluster: %s", cName), nil
1002 }); err != nil {
1003 http.Error(w, err.Error(), http.StatusInternalServerError)
1004 return
1005 }
1006 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
1007}
1008
gio59946282024-10-07 12:55:51 +04001009func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +04001010 cName, ok := mux.Vars(r)["name"]
1011 if !ok {
1012 http.Error(w, "empty name", http.StatusBadRequest)
1013 return
1014 }
gio8c876172024-10-05 12:25:13 +04001015 tid := 0
1016 if t, ok := s.tasks[cName]; ok {
1017 if t.task != nil {
1018 http.Error(w, "cluster task in progress", http.StatusLocked)
1019 return
1020 }
1021 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +04001022 }
1023 m, err := s.getClusterManager(cName)
1024 if err != nil {
1025 if errors.Is(err, installer.ErrorNotFound) {
1026 http.Error(w, "not found", http.StatusNotFound)
1027 } else {
1028 http.Error(w, err.Error(), http.StatusInternalServerError)
1029 }
1030 return
1031 }
1032 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +04001033 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +04001034 go task.Start()
gio8c876172024-10-05 12:25:13 +04001035 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +04001036 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
1037}
1038
gio59946282024-10-07 12:55:51 +04001039func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001040 const vpnUser = "private-network-proxy"
1041 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1042 hostname := fmt.Sprintf("cluster-%s", name)
1043 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1044 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1045 if err != nil {
1046 return installer.ReleaseResources{}, err
1047 }
1048 env, err := s.m.Config()
1049 if err != nil {
1050 return installer.ReleaseResources{}, err
1051 }
gio721c0042025-04-03 11:56:36 +04001052 keys, err := installer.NewSSHKeyPair("port-allocator")
1053 if err != nil {
1054 return installer.ReleaseResources{}, err
1055 }
1056 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1057 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1058 return installer.ReleaseResources{}, err
1059 }
1060 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1061 return installer.ReleaseResources{}, err
1062 }
giof6ad2982024-08-23 17:42:49 +04001063 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1064 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001065 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001066 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1067 "cluster": map[string]any{
1068 "name": name,
1069 "kubeconfig": kubeconfig,
1070 "ingressClassName": ingressClassName,
1071 },
1072 // TODO(gio): remove hardcoded user
1073 "vpnUser": vpnUser,
1074 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001075 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001076 })
1077 if err != nil {
1078 return installer.ReleaseResources{}, err
1079 }
1080 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1081 go s.reconciler.Reconcile(ctx)
1082 return rr, err
1083 })
1084 ch := make(chan error)
1085 t.OnDone(func(err error) {
1086 ch <- err
1087 })
1088 go t.Start()
1089 err := <-ch
1090 if err != nil {
1091 return nil, err
1092 }
1093 for {
1094 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1095 if err == nil {
1096 return ip, nil
1097 }
1098 if errors.Is(err, installer.ErrorNotFound) {
1099 time.Sleep(5 * time.Second)
1100 }
1101 }
1102 }
1103}
gio8f290322024-09-21 15:37:45 +04001104
gio59946282024-10-07 12:55:51 +04001105func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001106 return func(cm cluster.Manager) error {
1107 name := cm.State().Name
1108 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1109 app, err := installer.FindEnvApp(s.fr, "longhorn")
1110 if err != nil {
1111 return installer.ReleaseResources{}, err
1112 }
1113 env, err := s.m.Config()
1114 if err != nil {
1115 return installer.ReleaseResources{}, err
1116 }
1117 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1118 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1119 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1120 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1121 "cluster": name,
1122 })
1123 if err != nil {
1124 return installer.ReleaseResources{}, err
1125 }
1126 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1127 go s.reconciler.Reconcile(ctx)
1128 return rr, err
1129 })
1130 ch := make(chan error)
1131 t.OnDone(func(err error) {
1132 ch <- err
1133 })
1134 go t.Start()
1135 err := <-ch
1136 if err != nil {
1137 return err
1138 }
1139 cm.EnableStorage()
1140 return nil
1141 }
1142}
gio8c876172024-10-05 12:25:13 +04001143
gio59946282024-10-07 12:55:51 +04001144func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001145 return func(err error) {
1146 if err != nil {
1147 fmt.Printf("Task %s failed: %s", name, err.Error())
1148 }
1149 s.l.Lock()
1150 defer s.l.Unlock()
1151 s.tasks[name].task = nil
1152 go func() {
1153 time.Sleep(30 * time.Second)
1154 s.l.Lock()
1155 defer s.l.Unlock()
1156 if t, ok := s.tasks[name]; ok && t.id == id {
1157 delete(s.tasks, name)
1158 }
1159 }()
1160 }
1161}