blob: b26fc85201bd6e3393468700c2bebd9fa9236fb1 [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"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040025 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040026)
27
gio59946282024-10-07 12:55:51 +040028//go:embed templates/*
29var templates embed.FS
30
31//go:embed static/*
32var staticAssets embed.FS
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040033
giof6ad2982024-08-23 17:42:49 +040034type taskForward struct {
35 task tasks.Task
36 redirectTo string
gio8c876172024-10-05 12:25:13 +040037 id int
giof6ad2982024-08-23 17:42:49 +040038}
39
gio59946282024-10-07 12:55:51 +040040type Server struct {
giof6ad2982024-08-23 17:42:49 +040041 l sync.Locker
42 port int
gio721c0042025-04-03 11:56:36 +040043 ssClient soft.Client
giof6ad2982024-08-23 17:42:49 +040044 repo soft.RepoIO
45 m *installer.AppManager
46 r installer.AppRepository
47 fr installer.AppRepository
48 reconciler *tasks.FixedReconciler
49 h installer.HelmReleaseMonitor
50 cnc installer.ClusterNetworkConfigurator
51 vpnAPIClient installer.VPNAPIClient
gio8c876172024-10-05 12:25:13 +040052 tasks map[string]*taskForward
giof6ad2982024-08-23 17:42:49 +040053 tmpl tmplts
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040054}
55
56type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040057 index *template.Template
58 app *template.Template
59 allClusters *template.Template
60 cluster *template.Template
61 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040062}
63
gio59946282024-10-07 12:55:51 +040064func parseTemplates(fs embed.FS) (tmplts, error) {
65 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "templates/base.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040066 if err != nil {
67 return tmplts{}, err
68 }
69 parse := func(path string) (*template.Template, error) {
70 if b, err := base.Clone(); err != nil {
71 return nil, err
72 } else {
73 return b.ParseFS(fs, path)
74 }
75 }
gio59946282024-10-07 12:55:51 +040076 index, err := parse("templates/index.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040077 if err != nil {
78 return tmplts{}, err
79 }
gio59946282024-10-07 12:55:51 +040080 app, err := parse("templates/app.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040081 if err != nil {
82 return tmplts{}, err
83 }
gio59946282024-10-07 12:55:51 +040084 allClusters, err := parse("templates/all-clusters.html")
giof6ad2982024-08-23 17:42:49 +040085 if err != nil {
86 return tmplts{}, err
87 }
gio59946282024-10-07 12:55:51 +040088 cluster, err := parse("templates/cluster.html")
giof6ad2982024-08-23 17:42:49 +040089 if err != nil {
90 return tmplts{}, err
91 }
gio59946282024-10-07 12:55:51 +040092 task, err := parse("templates/task.html")
giof6ad2982024-08-23 17:42:49 +040093 if err != nil {
94 return tmplts{}, err
95 }
96 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040097}
98
gio59946282024-10-07 12:55:51 +040099func NewServer(
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400100 port int,
gio721c0042025-04-03 11:56:36 +0400101 ssClient soft.Client,
giof6ad2982024-08-23 17:42:49 +0400102 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400103 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400104 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400105 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400106 reconciler *tasks.FixedReconciler,
gio778577f2024-04-29 09:44:38 +0400107 h installer.HelmReleaseMonitor,
giof6ad2982024-08-23 17:42:49 +0400108 cnc installer.ClusterNetworkConfigurator,
109 vpnAPIClient installer.VPNAPIClient,
gio59946282024-10-07 12:55:51 +0400110) (*Server, error) {
111 tmpl, err := parseTemplates(templates)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400112 if err != nil {
113 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400114 }
gio59946282024-10-07 12:55:51 +0400115 return &Server{
giof6ad2982024-08-23 17:42:49 +0400116 l: &sync.Mutex{},
117 port: port,
gio721c0042025-04-03 11:56:36 +0400118 ssClient: ssClient,
giof6ad2982024-08-23 17:42:49 +0400119 repo: repo,
120 m: m,
121 r: r,
122 fr: fr,
123 reconciler: reconciler,
124 h: h,
125 cnc: cnc,
126 vpnAPIClient: vpnAPIClient,
gio8c876172024-10-05 12:25:13 +0400127 tasks: make(map[string]*taskForward),
giof6ad2982024-08-23 17:42:49 +0400128 tmpl: tmpl,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400129 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400130}
131
gio59946282024-10-07 12:55:51 +0400132func (s *Server) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400133 r := mux.NewRouter()
gio59946282024-10-07 12:55:51 +0400134 r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
giocb34ad22024-07-11 08:01:13 +0400135 r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
giof15b9da2024-09-19 06:59:16 +0400136 r.HandleFunc("/api/clusters", s.handleClusters).Methods(http.MethodGet)
137 r.HandleFunc("/api/proxy/add", s.handleProxyAdd).Methods(http.MethodPost)
138 r.HandleFunc("/api/proxy/remove", s.handleProxyRemove).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400139 r.HandleFunc("/api/app-repo", s.handleAppRepo)
140 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
141 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
142 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
143 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
144 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
gio268787a2025-04-24 21:18:06 +0400145 r.HandleFunc("/api/tasks/{instanceId}", s.handleTaskStatusAPI).Methods(http.MethodGet)
gio63a1a822025-04-23 12:59:40 +0400146 r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
giofc441e32024-11-11 16:26:14 +0400147 r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400148 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
149 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
150 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400151 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400152 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
153 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
154 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400155 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
156 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400157 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400158 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
159 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400160 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400161 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400162}
163
giofc441e32024-11-11 16:26:14 +0400164type dodoAppInstallReq struct {
gio74e73e92025-04-20 11:57:44 +0400165 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400166}
167
gio218e8132025-04-22 17:11:58 +0000168type dodoAppInstallResp struct {
169 Id string `json:"id"`
170 DeployKey string `json:"deployKey"`
171}
172
173type dodoAppRendered struct {
174 Input struct {
175 Key struct {
176 Public string `json:"public"`
177 } `json:"key"`
178 } `json:"input"`
179}
180
giofc441e32024-11-11 16:26:14 +0400181func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400182 s.l.Lock()
183 defer s.l.Unlock()
giofc441e32024-11-11 16:26:14 +0400184 var req dodoAppInstallReq
185 // TODO(gio): validate that no internal fields are overridden by request
186 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
187 http.Error(w, err.Error(), http.StatusBadRequest)
188 return
189 }
190 clusters, err := s.m.GetClusters()
191 if err != nil {
192 http.Error(w, err.Error(), http.StatusInternalServerError)
193 return
194 }
195 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
196 var cfg bytes.Buffer
197 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
198 http.Error(w, err.Error(), http.StatusInternalServerError)
199 return
200 }
201 app, err := installer.NewDodoApp(cfg.Bytes())
202 if err != nil {
203 http.Error(w, err.Error(), http.StatusBadRequest)
204 return
205 }
gio218e8132025-04-22 17:11:58 +0000206 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400207 http.Error(w, err.Error(), http.StatusInternalServerError)
208 return
gioa421b062025-04-21 09:45:04 +0400209 } else {
gio218e8132025-04-22 17:11:58 +0000210 var cfg dodoAppRendered
211 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
212 http.Error(w, err.Error(), http.StatusInternalServerError)
213 }
214 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
215 Id: instanceId,
216 DeployKey: cfg.Input.Key.Public,
217 }); err != nil {
218 http.Error(w, err.Error(), http.StatusInternalServerError)
219 }
giofc441e32024-11-11 16:26:14 +0400220 }
221}
222
gio63a1a822025-04-23 12:59:40 +0400223func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
gio268787a2025-04-24 21:18:06 +0400224 s.l.Lock()
225 defer s.l.Unlock()
gio63a1a822025-04-23 12:59:40 +0400226 instanceId, ok := mux.Vars(r)["instanceId"]
227 if !ok {
228 http.Error(w, "missing instance id", http.StatusBadRequest)
229 }
gio268787a2025-04-24 21:18:06 +0400230 if _, ok := s.tasks[instanceId]; ok {
231 http.Error(w, "task in progress", http.StatusTooEarly)
232 return
233 }
gio63a1a822025-04-23 12:59:40 +0400234 var req dodoAppInstallReq
235 // TODO(gio): validate that no internal fields are overridden by request
236 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
237 http.Error(w, err.Error(), http.StatusBadRequest)
238 return
239 }
240 clusters, err := s.m.GetClusters()
241 if err != nil {
242 http.Error(w, err.Error(), http.StatusInternalServerError)
243 return
244 }
245 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
246 var cfg bytes.Buffer
247 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
248 http.Error(w, err.Error(), http.StatusInternalServerError)
249 return
250 }
251 overrides := installer.CueAppData{
252 "app.cue": cfg.Bytes(),
253 }
gio268787a2025-04-24 21:18:06 +0400254 rr, err := s.m.Update(instanceId, nil, overrides)
255 if err != nil {
gio63a1a822025-04-23 12:59:40 +0400256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 }
gio268787a2025-04-24 21:18:06 +0400258 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
259 if err == nil {
260 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
261 go s.reconciler.Reconcile(ctx)
262 }
263 return rr, err
264 })
265 if _, ok := s.tasks[instanceId]; ok {
266 panic("MUST NOT REACH!")
267 }
268 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
269 t.OnDone(s.cleanTask(instanceId, 0))
270 go t.Start()
gio63a1a822025-04-23 12:59:40 +0400271}
272
gio59946282024-10-07 12:55:51 +0400273func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400274 env, err := s.m.Config()
275 if err != nil {
276 http.Error(w, err.Error(), http.StatusInternalServerError)
277 return
278 }
279 networks, err := s.m.CreateNetworks(env)
280 if err != nil {
281 http.Error(w, err.Error(), http.StatusInternalServerError)
282 return
283 }
284 if err := json.NewEncoder(w).Encode(networks); err != nil {
285 http.Error(w, err.Error(), http.StatusInternalServerError)
286 return
287 }
288}
289
gio59946282024-10-07 12:55:51 +0400290func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400291 clusters, err := s.m.GetClusters()
292 if err != nil {
293 http.Error(w, err.Error(), http.StatusInternalServerError)
294 return
295 }
296 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
297 http.Error(w, err.Error(), http.StatusInternalServerError)
298 return
299 }
300}
301
302type proxyPair struct {
303 From string `json:"from"`
304 To string `json:"to"`
305}
306
gio59946282024-10-07 12:55:51 +0400307func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400308 var req proxyPair
309 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
310 http.Error(w, err.Error(), http.StatusBadRequest)
311 return
312 }
gio721c0042025-04-03 11:56:36 +0400313 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400314 http.Error(w, err.Error(), http.StatusInternalServerError)
315 return
316 }
317}
318
gio59946282024-10-07 12:55:51 +0400319func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400320 var req proxyPair
321 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
322 http.Error(w, err.Error(), http.StatusBadRequest)
323 return
324 }
gio721c0042025-04-03 11:56:36 +0400325 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400326 http.Error(w, err.Error(), http.StatusInternalServerError)
327 return
328 }
329}
330
331type app struct {
332 Name string `json:"name"`
333 Icon template.HTML `json:"icon"`
334 ShortDescription string `json:"shortDescription"`
335 Slug string `json:"slug"`
336 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
337}
338
gio59946282024-10-07 12:55:51 +0400339func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400340 all, err := s.r.GetAll()
341 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400342 http.Error(w, err.Error(), http.StatusInternalServerError)
343 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400344 }
345 resp := make([]app, len(all))
346 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400347 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400348 }
gioaa0fcdb2024-06-10 22:19:25 +0400349 w.Header().Set("Content-Type", "application/json")
350 if err := json.NewEncoder(w).Encode(resp); err != nil {
351 http.Error(w, err.Error(), http.StatusInternalServerError)
352 return
353 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400354}
355
gio59946282024-10-07 12:55:51 +0400356func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400357 slug, ok := mux.Vars(r)["slug"]
358 if !ok {
359 http.Error(w, "empty slug", http.StatusBadRequest)
360 return
361 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400362 a, err := s.r.Find(slug)
363 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400364 http.Error(w, err.Error(), http.StatusInternalServerError)
365 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400366 }
gio7fbd4ad2024-08-27 10:06:39 +0400367 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400368 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400371 }
gioaa0fcdb2024-06-10 22:19:25 +0400372 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
373 w.Header().Set("Content-Type", "application/json")
374 if err := json.NewEncoder(w).Encode(resp); err != nil {
375 http.Error(w, err.Error(), http.StatusInternalServerError)
376 return
377 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400378}
379
gio59946282024-10-07 12:55:51 +0400380func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400381 slug, ok := mux.Vars(r)["slug"]
382 if !ok {
383 http.Error(w, "empty slug", http.StatusBadRequest)
384 return
385 }
gio7fbd4ad2024-08-27 10:06:39 +0400386 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400387 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400388 http.Error(w, err.Error(), http.StatusInternalServerError)
389 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400390 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400391 a, err := s.r.Find(instance.AppId)
392 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400393 http.Error(w, err.Error(), http.StatusInternalServerError)
394 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395 }
gioaa0fcdb2024-06-10 22:19:25 +0400396 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
397 w.Header().Set("Content-Type", "application/json")
398 if err := json.NewEncoder(w).Encode(resp); err != nil {
399 http.Error(w, err.Error(), http.StatusInternalServerError)
400 return
401 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400402}
403
gio218e8132025-04-22 17:11:58 +0000404func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400405 env, err := s.m.Config()
406 if err != nil {
gio218e8132025-04-22 17:11:58 +0000407 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400408 }
409 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
410 suffix, err := suffixGen.Generate()
411 if err != nil {
gio218e8132025-04-22 17:11:58 +0000412 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400413 }
414 instanceId := app.Slug() + suffix
415 appDir := fmt.Sprintf("/apps/%s", instanceId)
416 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000417 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400418 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400419 if err == nil {
420 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
421 go s.reconciler.Reconcile(ctx)
422 }
423 return rr, err
424 })
425 if _, ok := s.tasks[instanceId]; ok {
426 panic("MUST NOT REACH!")
427 }
428 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
429 t.OnDone(s.cleanTask(instanceId, 0))
430 go t.Start()
gio218e8132025-04-22 17:11:58 +0000431 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400432}
433
gio59946282024-10-07 12:55:51 +0400434func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400435 s.l.Lock()
436 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400437 slug, ok := mux.Vars(r)["slug"]
438 if !ok {
439 http.Error(w, "empty slug", http.StatusBadRequest)
440 return
441 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400442 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400443 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400444 http.Error(w, err.Error(), http.StatusInternalServerError)
445 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400446 }
gioa421b062025-04-21 09:45:04 +0400447 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400448 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400449 http.Error(w, err.Error(), http.StatusInternalServerError)
450 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400451 }
gio218e8132025-04-22 17:11:58 +0000452 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400453 http.Error(w, err.Error(), http.StatusInternalServerError)
454 return
gioa421b062025-04-21 09:45:04 +0400455 } else {
456 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400457 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400458}
459
gio59946282024-10-07 12:55:51 +0400460func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400461 s.l.Lock()
462 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400463 slug, ok := mux.Vars(r)["slug"]
464 if !ok {
465 http.Error(w, "empty slug", http.StatusBadRequest)
466 return
467 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400468 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400469 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400470 http.Error(w, err.Error(), http.StatusInternalServerError)
471 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400472 }
gio8c876172024-10-05 12:25:13 +0400473 tid := 0
474 if t, ok := s.tasks[slug]; ok {
475 if t.task != nil {
476 http.Error(w, "Update already in progress", http.StatusBadRequest)
477 return
478 }
479 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400480 }
gio63a1a822025-04-23 12:59:40 +0400481 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400482 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400483 http.Error(w, err.Error(), http.StatusInternalServerError)
484 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400485 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400486 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
487 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400488 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400489 t.OnDone(s.cleanTask(slug, tid))
490 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400491 go t.Start()
gio268787a2025-04-24 21:18:06 +0400492 fmt.Printf("Created task for %s\n", slug)
giof6ad2982024-08-23 17:42:49 +0400493 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400494 http.Error(w, err.Error(), http.StatusInternalServerError)
495 return
496 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400497}
498
gio59946282024-10-07 12:55:51 +0400499func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
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 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400506 http.Error(w, err.Error(), http.StatusInternalServerError)
507 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400508 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400509 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
510 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400511 if _, err := fmt.Fprint(w, "/"); err != nil {
512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
514 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400515}
516
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400517type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400518 Apps []app
519 CurrentPage string
520 SearchTarget string
521 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400522}
523
gio59946282024-10-07 12:55:51 +0400524func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400525 pageType := mux.Vars(r)["pageType"]
526 if pageType == "" {
527 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400528 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400529 searchQuery := r.FormValue("query")
530 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400531 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400532 http.Error(w, err.Error(), http.StatusInternalServerError)
533 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400534 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400535 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400536 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400537 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400538 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400539 http.Error(w, err.Error(), http.StatusInternalServerError)
540 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400541 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400542 switch pageType {
543 case "installed":
544 if len(instances) != 0 {
545 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
546 }
547 case "not-installed":
548 if len(instances) == 0 {
549 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
550 }
551 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400552 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
553 }
554 }
555 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400556 Apps: resp,
557 CurrentPage: pageType,
558 SearchTarget: pageType,
559 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400560 }
gioaa0fcdb2024-06-10 22:19:25 +0400561 if err := s.tmpl.index.Execute(w, data); err != nil {
562 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400563 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400564}
565
566type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400567 App installer.EnvApp
568 Instance *installer.AppInstanceConfig
569 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400570 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400571 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400572 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400573}
574
gio59946282024-10-07 12:55:51 +0400575func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400576 global, err := s.m.Config()
577 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400578 http.Error(w, err.Error(), http.StatusInternalServerError)
579 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400580 }
gioaa0fcdb2024-06-10 22:19:25 +0400581 slug, ok := mux.Vars(r)["slug"]
582 if !ok {
583 http.Error(w, "empty slug", http.StatusBadRequest)
584 return
585 }
gio3cdee592024-04-17 10:15:56 +0400586 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400587 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400588 http.Error(w, err.Error(), http.StatusInternalServerError)
589 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400590 }
gio7fbd4ad2024-08-27 10:06:39 +0400591 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400592 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400593 http.Error(w, err.Error(), http.StatusInternalServerError)
594 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400595 }
giocb34ad22024-07-11 08:01:13 +0400596 networks, err := s.m.CreateNetworks(global)
597 if err != nil {
598 http.Error(w, err.Error(), http.StatusInternalServerError)
599 return
600 }
giof6ad2982024-08-23 17:42:49 +0400601 clusters, err := s.m.GetClusters()
602 if err != nil {
603 http.Error(w, err.Error(), http.StatusInternalServerError)
604 return
605 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400606 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400607 App: a,
608 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400609 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400610 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400611 CurrentPage: a.Name(),
612 }
gioaa0fcdb2024-06-10 22:19:25 +0400613 if err := s.tmpl.app.Execute(w, data); err != nil {
614 http.Error(w, err.Error(), http.StatusInternalServerError)
615 return
616 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400617}
618
gio59946282024-10-07 12:55:51 +0400619func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400620 s.l.Lock()
621 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400622 global, err := s.m.Config()
623 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400624 http.Error(w, err.Error(), http.StatusInternalServerError)
625 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400626 }
gioaa0fcdb2024-06-10 22:19:25 +0400627 slug, ok := mux.Vars(r)["slug"]
628 if !ok {
629 http.Error(w, "empty slug", http.StatusBadRequest)
630 return
631 }
gio8c876172024-10-05 12:25:13 +0400632 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400633 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
634 return
635 }
gio8c876172024-10-05 12:25:13 +0400636 instance, err := s.m.GetInstance(slug)
637 if err != nil {
638 http.Error(w, err.Error(), http.StatusInternalServerError)
639 return
640 }
gio63a1a822025-04-23 12:59:40 +0400641 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400642 if err != nil {
643 http.Error(w, err.Error(), http.StatusInternalServerError)
644 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400645 }
gio7fbd4ad2024-08-27 10:06:39 +0400646 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400647 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400650 }
giocb34ad22024-07-11 08:01:13 +0400651 networks, err := s.m.CreateNetworks(global)
652 if err != nil {
653 http.Error(w, err.Error(), http.StatusInternalServerError)
654 return
655 }
giof6ad2982024-08-23 17:42:49 +0400656 clusters, err := s.m.GetClusters()
657 if err != nil {
658 http.Error(w, err.Error(), http.StatusInternalServerError)
659 return
660 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400661 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400662 App: a,
gio778577f2024-04-29 09:44:38 +0400663 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400664 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400665 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400666 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400667 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400668 }
gioaa0fcdb2024-06-10 22:19:25 +0400669 if err := s.tmpl.app.Execute(w, data); err != nil {
670 http.Error(w, err.Error(), http.StatusInternalServerError)
671 return
672 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400673}
giof6ad2982024-08-23 17:42:49 +0400674
675type taskStatusData struct {
676 CurrentPage string
677 Task tasks.Task
678}
679
gio59946282024-10-07 12:55:51 +0400680func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400681 s.l.Lock()
682 defer s.l.Unlock()
683 slug, ok := mux.Vars(r)["slug"]
684 if !ok {
685 http.Error(w, "empty slug", http.StatusBadRequest)
686 return
687 }
688 t, ok := s.tasks[slug]
689 if !ok {
690 http.Error(w, "task not found", http.StatusInternalServerError)
giof6ad2982024-08-23 17:42:49 +0400691 return
692 }
gio8c876172024-10-05 12:25:13 +0400693 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400694 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
695 return
696 }
697 data := taskStatusData{
698 CurrentPage: "",
699 Task: t.task,
700 }
701 if err := s.tmpl.task.Execute(w, data); err != nil {
702 http.Error(w, err.Error(), http.StatusInternalServerError)
703 return
704 }
705}
706
gio268787a2025-04-24 21:18:06 +0400707type resourceStatus struct {
708 Type string `json:"type"`
709 Name string `json:"name"`
710 Status string `json:"status"`
711}
712
713func extractResources(t tasks.Task) []resourceStatus {
714 var ret []resourceStatus
715 if t.Resource() != nil {
716 ret = append(ret, resourceStatus{
717 Type: t.Resource().Type,
718 Name: t.Resource().Name,
719 Status: tasks.StatusString(t.Status()),
720 })
721 }
722 for _, st := range t.Subtasks() {
723 ret = append(ret, extractResources(st)...)
724 }
725 return ret
726}
727
728func (s *Server) handleTaskStatusAPI(w http.ResponseWriter, r *http.Request) {
729 s.l.Lock()
730 defer s.l.Unlock()
731 instanceId, ok := mux.Vars(r)["instanceId"]
732 if !ok {
733 http.Error(w, "empty slug", http.StatusBadRequest)
734 return
735 }
736 t, ok := s.tasks[instanceId]
737 if !ok {
738 http.Error(w, "task not found", http.StatusInternalServerError)
739 return
740 }
741 if ok && t.task == nil {
742 http.Error(w, "not found", http.StatusNotFound)
743 return
744 }
745 resources := extractResources(t.task)
746 json.NewEncoder(w).Encode(resources)
747}
748
giof6ad2982024-08-23 17:42:49 +0400749type clustersData struct {
750 CurrentPage string
751 Clusters []cluster.State
752}
753
gio59946282024-10-07 12:55:51 +0400754func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400755 clusters, err := s.m.GetClusters()
756 if err != nil {
757 http.Error(w, err.Error(), http.StatusInternalServerError)
758 return
759 }
760 data := clustersData{
761 "clusters",
762 clusters,
763 }
764 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
765 http.Error(w, err.Error(), http.StatusInternalServerError)
766 return
767 }
768}
769
770type clusterData struct {
771 CurrentPage string
772 Cluster cluster.State
773}
774
gio59946282024-10-07 12:55:51 +0400775func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400776 name, ok := mux.Vars(r)["name"]
777 if !ok {
778 http.Error(w, "empty name", http.StatusBadRequest)
779 return
780 }
781 m, err := s.getClusterManager(name)
782 if err != nil {
783 if errors.Is(err, installer.ErrorNotFound) {
784 http.Error(w, "not found", http.StatusNotFound)
785 } else {
786 http.Error(w, err.Error(), http.StatusInternalServerError)
787 }
788 return
789 }
790 data := clusterData{
791 "clusters",
792 m.State(),
793 }
794 if err := s.tmpl.cluster.Execute(w, data); err != nil {
795 http.Error(w, err.Error(), http.StatusInternalServerError)
796 return
797 }
798}
799
gio59946282024-10-07 12:55:51 +0400800func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400801 cName, ok := mux.Vars(r)["name"]
802 if !ok {
803 http.Error(w, "empty name", http.StatusBadRequest)
804 return
805 }
gio8c876172024-10-05 12:25:13 +0400806 tid := 0
807 if t, ok := s.tasks[cName]; ok {
808 if t.task != nil {
809 http.Error(w, "cluster task in progress", http.StatusLocked)
810 return
811 }
812 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400813 }
814 m, err := s.getClusterManager(cName)
815 if err != nil {
816 if errors.Is(err, installer.ErrorNotFound) {
817 http.Error(w, "not found", http.StatusNotFound)
818 } else {
819 http.Error(w, err.Error(), http.StatusInternalServerError)
820 }
821 return
822 }
823 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400824 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400825 go task.Start()
gio8c876172024-10-05 12:25:13 +0400826 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400827 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
828}
829
gio59946282024-10-07 12:55:51 +0400830func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400831 s.l.Lock()
832 defer s.l.Unlock()
833 cName, ok := mux.Vars(r)["cluster"]
834 if !ok {
835 http.Error(w, "empty name", http.StatusBadRequest)
836 return
837 }
gio8c876172024-10-05 12:25:13 +0400838 tid := 0
839 if t, ok := s.tasks[cName]; ok {
840 if t.task != nil {
841 http.Error(w, "cluster task in progress", http.StatusLocked)
842 return
843 }
844 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400845 }
846 sName, ok := mux.Vars(r)["server"]
847 if !ok {
848 http.Error(w, "empty name", http.StatusBadRequest)
849 return
850 }
851 m, err := s.getClusterManager(cName)
852 if err != nil {
853 if errors.Is(err, installer.ErrorNotFound) {
854 http.Error(w, "not found", http.StatusNotFound)
855 } else {
856 http.Error(w, err.Error(), http.StatusInternalServerError)
857 }
858 return
859 }
860 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400861 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400862 go task.Start()
gio8c876172024-10-05 12:25:13 +0400863 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400864 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
865}
866
gio59946282024-10-07 12:55:51 +0400867func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400868 clusters, err := s.m.GetClusters()
869 if err != nil {
870 return nil, err
871 }
872 var c *cluster.State
873 for _, i := range clusters {
874 if i.Name == cName {
875 c = &i
876 break
877 }
878 }
879 if c == nil {
880 return nil, installer.ErrorNotFound
881 }
882 return cluster.RestoreKubeManager(*c)
883}
884
gio59946282024-10-07 12:55:51 +0400885func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400886 s.l.Lock()
887 defer s.l.Unlock()
888 cName, ok := mux.Vars(r)["cluster"]
889 if !ok {
890 http.Error(w, "empty name", http.StatusBadRequest)
891 return
892 }
gio8c876172024-10-05 12:25:13 +0400893 tid := 0
894 if t, ok := s.tasks[cName]; ok {
895 if t.task != nil {
896 http.Error(w, "cluster task in progress", http.StatusLocked)
897 return
898 }
899 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400900 }
901 m, err := s.getClusterManager(cName)
902 if err != nil {
903 if errors.Is(err, installer.ErrorNotFound) {
904 http.Error(w, "not found", http.StatusNotFound)
905 } else {
906 http.Error(w, err.Error(), http.StatusInternalServerError)
907 }
908 return
909 }
910 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400911 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400912 if ip == nil {
913 http.Error(w, "invalid ip", http.StatusBadRequest)
914 return
915 }
916 port := 22
917 if p := r.PostFormValue("port"); p != "" {
918 port, err = strconv.Atoi(p)
919 if err != nil {
920 http.Error(w, err.Error(), http.StatusBadRequest)
921 return
922 }
923 }
924 server := cluster.Server{
925 IP: ip,
926 Port: port,
927 User: r.PostFormValue("user"),
928 Password: r.PostFormValue("password"),
929 }
930 var task tasks.Task
931 switch strings.ToLower(t) {
932 case "controller":
933 if len(m.State().Controllers) == 0 {
934 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
935 } else {
936 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
937 }
938 case "worker":
939 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
940 default:
941 http.Error(w, "invalid type", http.StatusBadRequest)
942 return
943 }
gio8c876172024-10-05 12:25:13 +0400944 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400945 go task.Start()
gio8c876172024-10-05 12:25:13 +0400946 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400947 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
948}
949
gio59946282024-10-07 12:55:51 +0400950func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400951 cName := r.PostFormValue("name")
952 if cName == "" {
953 http.Error(w, "no name", http.StatusBadRequest)
954 return
955 }
956 st := cluster.State{Name: cName}
957 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
958 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
959 return "", err
960 }
961 return fmt.Sprintf("create cluster: %s", cName), nil
962 }); err != nil {
963 http.Error(w, err.Error(), http.StatusInternalServerError)
964 return
965 }
966 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
967}
968
gio59946282024-10-07 12:55:51 +0400969func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400970 cName, ok := mux.Vars(r)["name"]
971 if !ok {
972 http.Error(w, "empty name", http.StatusBadRequest)
973 return
974 }
gio8c876172024-10-05 12:25:13 +0400975 tid := 0
976 if t, ok := s.tasks[cName]; ok {
977 if t.task != nil {
978 http.Error(w, "cluster task in progress", http.StatusLocked)
979 return
980 }
981 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400982 }
983 m, err := s.getClusterManager(cName)
984 if err != nil {
985 if errors.Is(err, installer.ErrorNotFound) {
986 http.Error(w, "not found", http.StatusNotFound)
987 } else {
988 http.Error(w, err.Error(), http.StatusInternalServerError)
989 }
990 return
991 }
992 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400993 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400994 go task.Start()
gio8c876172024-10-05 12:25:13 +0400995 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400996 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
997}
998
gio59946282024-10-07 12:55:51 +0400999func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +04001000 const vpnUser = "private-network-proxy"
1001 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
1002 hostname := fmt.Sprintf("cluster-%s", name)
1003 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1004 app, err := installer.FindEnvApp(s.fr, "cluster-network")
1005 if err != nil {
1006 return installer.ReleaseResources{}, err
1007 }
1008 env, err := s.m.Config()
1009 if err != nil {
1010 return installer.ReleaseResources{}, err
1011 }
gio721c0042025-04-03 11:56:36 +04001012 keys, err := installer.NewSSHKeyPair("port-allocator")
1013 if err != nil {
1014 return installer.ReleaseResources{}, err
1015 }
1016 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
1017 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
1018 return installer.ReleaseResources{}, err
1019 }
1020 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
1021 return installer.ReleaseResources{}, err
1022 }
giof6ad2982024-08-23 17:42:49 +04001023 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1024 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +04001025 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +04001026 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1027 "cluster": map[string]any{
1028 "name": name,
1029 "kubeconfig": kubeconfig,
1030 "ingressClassName": ingressClassName,
1031 },
1032 // TODO(gio): remove hardcoded user
1033 "vpnUser": vpnUser,
1034 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +04001035 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +04001036 })
1037 if err != nil {
1038 return installer.ReleaseResources{}, err
1039 }
1040 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1041 go s.reconciler.Reconcile(ctx)
1042 return rr, err
1043 })
1044 ch := make(chan error)
1045 t.OnDone(func(err error) {
1046 ch <- err
1047 })
1048 go t.Start()
1049 err := <-ch
1050 if err != nil {
1051 return nil, err
1052 }
1053 for {
1054 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
1055 if err == nil {
1056 return ip, nil
1057 }
1058 if errors.Is(err, installer.ErrorNotFound) {
1059 time.Sleep(5 * time.Second)
1060 }
1061 }
1062 }
1063}
gio8f290322024-09-21 15:37:45 +04001064
gio59946282024-10-07 12:55:51 +04001065func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001066 return func(cm cluster.Manager) error {
1067 name := cm.State().Name
1068 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1069 app, err := installer.FindEnvApp(s.fr, "longhorn")
1070 if err != nil {
1071 return installer.ReleaseResources{}, err
1072 }
1073 env, err := s.m.Config()
1074 if err != nil {
1075 return installer.ReleaseResources{}, err
1076 }
1077 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1078 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1079 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1080 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1081 "cluster": name,
1082 })
1083 if err != nil {
1084 return installer.ReleaseResources{}, err
1085 }
1086 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1087 go s.reconciler.Reconcile(ctx)
1088 return rr, err
1089 })
1090 ch := make(chan error)
1091 t.OnDone(func(err error) {
1092 ch <- err
1093 })
1094 go t.Start()
1095 err := <-ch
1096 if err != nil {
1097 return err
1098 }
1099 cm.EnableStorage()
1100 return nil
1101 }
1102}
gio8c876172024-10-05 12:25:13 +04001103
gio59946282024-10-07 12:55:51 +04001104func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001105 return func(err error) {
1106 if err != nil {
1107 fmt.Printf("Task %s failed: %s", name, err.Error())
1108 }
1109 s.l.Lock()
1110 defer s.l.Unlock()
1111 s.tasks[name].task = nil
1112 go func() {
1113 time.Sleep(30 * time.Second)
1114 s.l.Lock()
1115 defer s.l.Unlock()
1116 if t, ok := s.tasks[name]; ok && t.id == id {
1117 delete(s.tasks, name)
1118 }
1119 }()
1120 }
1121}