blob: cb372f3fe470a62b2d09a0755aae908f14e1102c [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)
gio63a1a822025-04-23 12:59:40 +0400145 r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
giofc441e32024-11-11 16:26:14 +0400146 r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400147 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
148 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
149 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400150 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400151 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
152 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
153 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400154 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
155 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400156 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400157 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
158 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400159 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400160 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400161}
162
giofc441e32024-11-11 16:26:14 +0400163type dodoAppInstallReq struct {
gio74e73e92025-04-20 11:57:44 +0400164 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400165}
166
gio218e8132025-04-22 17:11:58 +0000167type dodoAppInstallResp struct {
168 Id string `json:"id"`
169 DeployKey string `json:"deployKey"`
170}
171
172type dodoAppRendered struct {
173 Input struct {
174 Key struct {
175 Public string `json:"public"`
176 } `json:"key"`
177 } `json:"input"`
178}
179
giofc441e32024-11-11 16:26:14 +0400180func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
181 var req dodoAppInstallReq
182 // TODO(gio): validate that no internal fields are overridden by request
183 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
184 http.Error(w, err.Error(), http.StatusBadRequest)
185 return
186 }
187 clusters, err := s.m.GetClusters()
188 if err != nil {
189 http.Error(w, err.Error(), http.StatusInternalServerError)
190 return
191 }
192 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
193 var cfg bytes.Buffer
194 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
195 http.Error(w, err.Error(), http.StatusInternalServerError)
196 return
197 }
198 app, err := installer.NewDodoApp(cfg.Bytes())
199 if err != nil {
200 http.Error(w, err.Error(), http.StatusBadRequest)
201 return
202 }
gio218e8132025-04-22 17:11:58 +0000203 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400204 http.Error(w, err.Error(), http.StatusInternalServerError)
205 return
gioa421b062025-04-21 09:45:04 +0400206 } else {
gio218e8132025-04-22 17:11:58 +0000207 var cfg dodoAppRendered
208 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
209 http.Error(w, err.Error(), http.StatusInternalServerError)
210 }
211 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
212 Id: instanceId,
213 DeployKey: cfg.Input.Key.Public,
214 }); err != nil {
215 http.Error(w, err.Error(), http.StatusInternalServerError)
216 }
giofc441e32024-11-11 16:26:14 +0400217 }
218}
219
gio63a1a822025-04-23 12:59:40 +0400220func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
221 instanceId, ok := mux.Vars(r)["instanceId"]
222 if !ok {
223 http.Error(w, "missing instance id", http.StatusBadRequest)
224 }
225 var req dodoAppInstallReq
226 // TODO(gio): validate that no internal fields are overridden by request
227 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
228 http.Error(w, err.Error(), http.StatusBadRequest)
229 return
230 }
231 clusters, err := s.m.GetClusters()
232 if err != nil {
233 http.Error(w, err.Error(), http.StatusInternalServerError)
234 return
235 }
236 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
237 var cfg bytes.Buffer
238 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
239 http.Error(w, err.Error(), http.StatusInternalServerError)
240 return
241 }
242 overrides := installer.CueAppData{
243 "app.cue": cfg.Bytes(),
244 }
245 // TODO(gio): return monitoring info
246 if _, err := s.m.Update(instanceId, nil, overrides); err != nil {
247 http.Error(w, err.Error(), http.StatusInternalServerError)
248 }
249}
250
gio59946282024-10-07 12:55:51 +0400251func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400252 env, err := s.m.Config()
253 if err != nil {
254 http.Error(w, err.Error(), http.StatusInternalServerError)
255 return
256 }
257 networks, err := s.m.CreateNetworks(env)
258 if err != nil {
259 http.Error(w, err.Error(), http.StatusInternalServerError)
260 return
261 }
262 if err := json.NewEncoder(w).Encode(networks); err != nil {
263 http.Error(w, err.Error(), http.StatusInternalServerError)
264 return
265 }
266}
267
gio59946282024-10-07 12:55:51 +0400268func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400269 clusters, err := s.m.GetClusters()
270 if err != nil {
271 http.Error(w, err.Error(), http.StatusInternalServerError)
272 return
273 }
274 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
275 http.Error(w, err.Error(), http.StatusInternalServerError)
276 return
277 }
278}
279
280type proxyPair struct {
281 From string `json:"from"`
282 To string `json:"to"`
283}
284
gio59946282024-10-07 12:55:51 +0400285func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400286 var req proxyPair
287 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
288 http.Error(w, err.Error(), http.StatusBadRequest)
289 return
290 }
gio721c0042025-04-03 11:56:36 +0400291 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400292 http.Error(w, err.Error(), http.StatusInternalServerError)
293 return
294 }
295}
296
gio59946282024-10-07 12:55:51 +0400297func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400298 var req proxyPair
299 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
300 http.Error(w, err.Error(), http.StatusBadRequest)
301 return
302 }
gio721c0042025-04-03 11:56:36 +0400303 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400304 http.Error(w, err.Error(), http.StatusInternalServerError)
305 return
306 }
307}
308
309type app struct {
310 Name string `json:"name"`
311 Icon template.HTML `json:"icon"`
312 ShortDescription string `json:"shortDescription"`
313 Slug string `json:"slug"`
314 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
315}
316
gio59946282024-10-07 12:55:51 +0400317func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318 all, err := s.r.GetAll()
319 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400320 http.Error(w, err.Error(), http.StatusInternalServerError)
321 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400322 }
323 resp := make([]app, len(all))
324 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400325 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400326 }
gioaa0fcdb2024-06-10 22:19:25 +0400327 w.Header().Set("Content-Type", "application/json")
328 if err := json.NewEncoder(w).Encode(resp); err != nil {
329 http.Error(w, err.Error(), http.StatusInternalServerError)
330 return
331 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400332}
333
gio59946282024-10-07 12:55:51 +0400334func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400335 slug, ok := mux.Vars(r)["slug"]
336 if !ok {
337 http.Error(w, "empty slug", http.StatusBadRequest)
338 return
339 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400340 a, err := s.r.Find(slug)
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 }
gio7fbd4ad2024-08-27 10:06:39 +0400345 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400346 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400347 http.Error(w, err.Error(), http.StatusInternalServerError)
348 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400349 }
gioaa0fcdb2024-06-10 22:19:25 +0400350 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
351 w.Header().Set("Content-Type", "application/json")
352 if err := json.NewEncoder(w).Encode(resp); err != nil {
353 http.Error(w, err.Error(), http.StatusInternalServerError)
354 return
355 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400356}
357
gio59946282024-10-07 12:55:51 +0400358func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400359 slug, ok := mux.Vars(r)["slug"]
360 if !ok {
361 http.Error(w, "empty slug", http.StatusBadRequest)
362 return
363 }
gio7fbd4ad2024-08-27 10:06:39 +0400364 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400365 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400366 http.Error(w, err.Error(), http.StatusInternalServerError)
367 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400368 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400369 a, err := s.r.Find(instance.AppId)
370 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400371 http.Error(w, err.Error(), http.StatusInternalServerError)
372 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400373 }
gioaa0fcdb2024-06-10 22:19:25 +0400374 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
375 w.Header().Set("Content-Type", "application/json")
376 if err := json.NewEncoder(w).Encode(resp); err != nil {
377 http.Error(w, err.Error(), http.StatusInternalServerError)
378 return
379 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400380}
381
gio218e8132025-04-22 17:11:58 +0000382func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400383 env, err := s.m.Config()
384 if err != nil {
gio218e8132025-04-22 17:11:58 +0000385 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400386 }
387 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
388 suffix, err := suffixGen.Generate()
389 if err != nil {
gio218e8132025-04-22 17:11:58 +0000390 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400391 }
392 instanceId := app.Slug() + suffix
393 appDir := fmt.Sprintf("/apps/%s", instanceId)
394 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000395 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400396 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400397 if err == nil {
398 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
399 go s.reconciler.Reconcile(ctx)
400 }
401 return rr, err
402 })
403 if _, ok := s.tasks[instanceId]; ok {
404 panic("MUST NOT REACH!")
405 }
406 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
407 t.OnDone(s.cleanTask(instanceId, 0))
408 go t.Start()
gio218e8132025-04-22 17:11:58 +0000409 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400410}
411
gio59946282024-10-07 12:55:51 +0400412func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400413 s.l.Lock()
414 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400415 slug, ok := mux.Vars(r)["slug"]
416 if !ok {
417 http.Error(w, "empty slug", http.StatusBadRequest)
418 return
419 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400420 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400421 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400422 http.Error(w, err.Error(), http.StatusInternalServerError)
423 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400424 }
gioa421b062025-04-21 09:45:04 +0400425 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400426 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400427 http.Error(w, err.Error(), http.StatusInternalServerError)
428 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400429 }
gio218e8132025-04-22 17:11:58 +0000430 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400431 http.Error(w, err.Error(), http.StatusInternalServerError)
432 return
gioa421b062025-04-21 09:45:04 +0400433 } else {
434 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400435 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400436}
437
gio59946282024-10-07 12:55:51 +0400438func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400439 s.l.Lock()
440 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400441 slug, ok := mux.Vars(r)["slug"]
442 if !ok {
443 http.Error(w, "empty slug", http.StatusBadRequest)
444 return
445 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400446 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400447 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400448 http.Error(w, err.Error(), http.StatusInternalServerError)
449 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400450 }
gio8c876172024-10-05 12:25:13 +0400451 tid := 0
452 if t, ok := s.tasks[slug]; ok {
453 if t.task != nil {
454 http.Error(w, "Update already in progress", http.StatusBadRequest)
455 return
456 }
457 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400458 }
gio63a1a822025-04-23 12:59:40 +0400459 rr, err := s.m.Update(slug, values, nil)
gio778577f2024-04-29 09:44:38 +0400460 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400461 http.Error(w, err.Error(), http.StatusInternalServerError)
462 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400463 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400464 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
465 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400466 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400467 t.OnDone(s.cleanTask(slug, tid))
468 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400469 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400470 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400471 http.Error(w, err.Error(), http.StatusInternalServerError)
472 return
473 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400474}
475
gio59946282024-10-07 12:55:51 +0400476func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400477 slug, ok := mux.Vars(r)["slug"]
478 if !ok {
479 http.Error(w, "empty slug", http.StatusBadRequest)
480 return
481 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400482 if err := s.m.Remove(slug); 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)
gioaa0fcdb2024-06-10 22:19:25 +0400488 if _, err := fmt.Fprint(w, "/"); err != nil {
489 http.Error(w, err.Error(), http.StatusInternalServerError)
490 return
491 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400492}
493
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400494type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400495 Apps []app
496 CurrentPage string
497 SearchTarget string
498 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400499}
500
gio59946282024-10-07 12:55:51 +0400501func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400502 pageType := mux.Vars(r)["pageType"]
503 if pageType == "" {
504 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400505 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400506 searchQuery := r.FormValue("query")
507 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400508 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400509 http.Error(w, err.Error(), http.StatusInternalServerError)
510 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400511 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400512 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400513 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400514 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400515 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400516 http.Error(w, err.Error(), http.StatusInternalServerError)
517 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400518 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400519 switch pageType {
520 case "installed":
521 if len(instances) != 0 {
522 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
523 }
524 case "not-installed":
525 if len(instances) == 0 {
526 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
527 }
528 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400529 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
530 }
531 }
532 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400533 Apps: resp,
534 CurrentPage: pageType,
535 SearchTarget: pageType,
536 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400537 }
gioaa0fcdb2024-06-10 22:19:25 +0400538 if err := s.tmpl.index.Execute(w, data); err != nil {
539 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400540 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400541}
542
543type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400544 App installer.EnvApp
545 Instance *installer.AppInstanceConfig
546 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400547 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400548 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400549 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400550}
551
gio59946282024-10-07 12:55:51 +0400552func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400553 global, err := s.m.Config()
554 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400555 http.Error(w, err.Error(), http.StatusInternalServerError)
556 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400557 }
gioaa0fcdb2024-06-10 22:19:25 +0400558 slug, ok := mux.Vars(r)["slug"]
559 if !ok {
560 http.Error(w, "empty slug", http.StatusBadRequest)
561 return
562 }
gio3cdee592024-04-17 10:15:56 +0400563 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400564 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400565 http.Error(w, err.Error(), http.StatusInternalServerError)
566 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400567 }
gio7fbd4ad2024-08-27 10:06:39 +0400568 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400569 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400570 http.Error(w, err.Error(), http.StatusInternalServerError)
571 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400572 }
giocb34ad22024-07-11 08:01:13 +0400573 networks, err := s.m.CreateNetworks(global)
574 if err != nil {
575 http.Error(w, err.Error(), http.StatusInternalServerError)
576 return
577 }
giof6ad2982024-08-23 17:42:49 +0400578 clusters, err := s.m.GetClusters()
579 if err != nil {
580 http.Error(w, err.Error(), http.StatusInternalServerError)
581 return
582 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400583 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400584 App: a,
585 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400586 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400587 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400588 CurrentPage: a.Name(),
589 }
gioaa0fcdb2024-06-10 22:19:25 +0400590 if err := s.tmpl.app.Execute(w, data); err != nil {
591 http.Error(w, err.Error(), http.StatusInternalServerError)
592 return
593 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400594}
595
gio59946282024-10-07 12:55:51 +0400596func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400597 s.l.Lock()
598 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400599 global, err := s.m.Config()
600 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400601 http.Error(w, err.Error(), http.StatusInternalServerError)
602 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400603 }
gioaa0fcdb2024-06-10 22:19:25 +0400604 slug, ok := mux.Vars(r)["slug"]
605 if !ok {
606 http.Error(w, "empty slug", http.StatusBadRequest)
607 return
608 }
gio8c876172024-10-05 12:25:13 +0400609 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400610 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
611 return
612 }
gio8c876172024-10-05 12:25:13 +0400613 instance, err := s.m.GetInstance(slug)
614 if err != nil {
615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
617 }
gio63a1a822025-04-23 12:59:40 +0400618 a, err := s.m.GetInstanceApp(instance.Id, nil)
gio8c876172024-10-05 12:25:13 +0400619 if err != nil {
620 http.Error(w, err.Error(), http.StatusInternalServerError)
621 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400622 }
gio7fbd4ad2024-08-27 10:06:39 +0400623 instances, err := s.m.GetAllAppInstances(a.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 }
giocb34ad22024-07-11 08:01:13 +0400628 networks, err := s.m.CreateNetworks(global)
629 if err != nil {
630 http.Error(w, err.Error(), http.StatusInternalServerError)
631 return
632 }
giof6ad2982024-08-23 17:42:49 +0400633 clusters, err := s.m.GetClusters()
634 if err != nil {
635 http.Error(w, err.Error(), http.StatusInternalServerError)
636 return
637 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400638 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400639 App: a,
gio778577f2024-04-29 09:44:38 +0400640 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400641 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400642 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400643 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400644 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400645 }
gioaa0fcdb2024-06-10 22:19:25 +0400646 if err := s.tmpl.app.Execute(w, data); err != nil {
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400650}
giof6ad2982024-08-23 17:42:49 +0400651
652type taskStatusData struct {
653 CurrentPage string
654 Task tasks.Task
655}
656
gio59946282024-10-07 12:55:51 +0400657func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400658 s.l.Lock()
659 defer s.l.Unlock()
660 slug, ok := mux.Vars(r)["slug"]
661 if !ok {
662 http.Error(w, "empty slug", http.StatusBadRequest)
663 return
664 }
665 t, ok := s.tasks[slug]
666 if !ok {
667 http.Error(w, "task not found", http.StatusInternalServerError)
668
669 return
670 }
gio8c876172024-10-05 12:25:13 +0400671 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400672 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
673 return
674 }
675 data := taskStatusData{
676 CurrentPage: "",
677 Task: t.task,
678 }
679 if err := s.tmpl.task.Execute(w, data); err != nil {
680 http.Error(w, err.Error(), http.StatusInternalServerError)
681 return
682 }
683}
684
685type clustersData struct {
686 CurrentPage string
687 Clusters []cluster.State
688}
689
gio59946282024-10-07 12:55:51 +0400690func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400691 clusters, err := s.m.GetClusters()
692 if err != nil {
693 http.Error(w, err.Error(), http.StatusInternalServerError)
694 return
695 }
696 data := clustersData{
697 "clusters",
698 clusters,
699 }
700 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
701 http.Error(w, err.Error(), http.StatusInternalServerError)
702 return
703 }
704}
705
706type clusterData struct {
707 CurrentPage string
708 Cluster cluster.State
709}
710
gio59946282024-10-07 12:55:51 +0400711func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400712 name, ok := mux.Vars(r)["name"]
713 if !ok {
714 http.Error(w, "empty name", http.StatusBadRequest)
715 return
716 }
717 m, err := s.getClusterManager(name)
718 if err != nil {
719 if errors.Is(err, installer.ErrorNotFound) {
720 http.Error(w, "not found", http.StatusNotFound)
721 } else {
722 http.Error(w, err.Error(), http.StatusInternalServerError)
723 }
724 return
725 }
726 data := clusterData{
727 "clusters",
728 m.State(),
729 }
730 if err := s.tmpl.cluster.Execute(w, data); err != nil {
731 http.Error(w, err.Error(), http.StatusInternalServerError)
732 return
733 }
734}
735
gio59946282024-10-07 12:55:51 +0400736func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400737 cName, ok := mux.Vars(r)["name"]
738 if !ok {
739 http.Error(w, "empty name", http.StatusBadRequest)
740 return
741 }
gio8c876172024-10-05 12:25:13 +0400742 tid := 0
743 if t, ok := s.tasks[cName]; ok {
744 if t.task != nil {
745 http.Error(w, "cluster task in progress", http.StatusLocked)
746 return
747 }
748 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400749 }
750 m, err := s.getClusterManager(cName)
751 if err != nil {
752 if errors.Is(err, installer.ErrorNotFound) {
753 http.Error(w, "not found", http.StatusNotFound)
754 } else {
755 http.Error(w, err.Error(), http.StatusInternalServerError)
756 }
757 return
758 }
759 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400760 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400761 go task.Start()
gio8c876172024-10-05 12:25:13 +0400762 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400763 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
764}
765
gio59946282024-10-07 12:55:51 +0400766func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400767 s.l.Lock()
768 defer s.l.Unlock()
769 cName, ok := mux.Vars(r)["cluster"]
770 if !ok {
771 http.Error(w, "empty name", http.StatusBadRequest)
772 return
773 }
gio8c876172024-10-05 12:25:13 +0400774 tid := 0
775 if t, ok := s.tasks[cName]; ok {
776 if t.task != nil {
777 http.Error(w, "cluster task in progress", http.StatusLocked)
778 return
779 }
780 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400781 }
782 sName, ok := mux.Vars(r)["server"]
783 if !ok {
784 http.Error(w, "empty name", http.StatusBadRequest)
785 return
786 }
787 m, err := s.getClusterManager(cName)
788 if err != nil {
789 if errors.Is(err, installer.ErrorNotFound) {
790 http.Error(w, "not found", http.StatusNotFound)
791 } else {
792 http.Error(w, err.Error(), http.StatusInternalServerError)
793 }
794 return
795 }
796 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400797 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400798 go task.Start()
gio8c876172024-10-05 12:25:13 +0400799 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400800 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
801}
802
gio59946282024-10-07 12:55:51 +0400803func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400804 clusters, err := s.m.GetClusters()
805 if err != nil {
806 return nil, err
807 }
808 var c *cluster.State
809 for _, i := range clusters {
810 if i.Name == cName {
811 c = &i
812 break
813 }
814 }
815 if c == nil {
816 return nil, installer.ErrorNotFound
817 }
818 return cluster.RestoreKubeManager(*c)
819}
820
gio59946282024-10-07 12:55:51 +0400821func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400822 s.l.Lock()
823 defer s.l.Unlock()
824 cName, ok := mux.Vars(r)["cluster"]
825 if !ok {
826 http.Error(w, "empty name", http.StatusBadRequest)
827 return
828 }
gio8c876172024-10-05 12:25:13 +0400829 tid := 0
830 if t, ok := s.tasks[cName]; ok {
831 if t.task != nil {
832 http.Error(w, "cluster task in progress", http.StatusLocked)
833 return
834 }
835 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400836 }
837 m, err := s.getClusterManager(cName)
838 if err != nil {
839 if errors.Is(err, installer.ErrorNotFound) {
840 http.Error(w, "not found", http.StatusNotFound)
841 } else {
842 http.Error(w, err.Error(), http.StatusInternalServerError)
843 }
844 return
845 }
846 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400847 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400848 if ip == nil {
849 http.Error(w, "invalid ip", http.StatusBadRequest)
850 return
851 }
852 port := 22
853 if p := r.PostFormValue("port"); p != "" {
854 port, err = strconv.Atoi(p)
855 if err != nil {
856 http.Error(w, err.Error(), http.StatusBadRequest)
857 return
858 }
859 }
860 server := cluster.Server{
861 IP: ip,
862 Port: port,
863 User: r.PostFormValue("user"),
864 Password: r.PostFormValue("password"),
865 }
866 var task tasks.Task
867 switch strings.ToLower(t) {
868 case "controller":
869 if len(m.State().Controllers) == 0 {
870 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
871 } else {
872 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
873 }
874 case "worker":
875 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
876 default:
877 http.Error(w, "invalid type", http.StatusBadRequest)
878 return
879 }
gio8c876172024-10-05 12:25:13 +0400880 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400881 go task.Start()
gio8c876172024-10-05 12:25:13 +0400882 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400883 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
884}
885
gio59946282024-10-07 12:55:51 +0400886func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400887 cName := r.PostFormValue("name")
888 if cName == "" {
889 http.Error(w, "no name", http.StatusBadRequest)
890 return
891 }
892 st := cluster.State{Name: cName}
893 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
894 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
895 return "", err
896 }
897 return fmt.Sprintf("create cluster: %s", cName), nil
898 }); err != nil {
899 http.Error(w, err.Error(), http.StatusInternalServerError)
900 return
901 }
902 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
903}
904
gio59946282024-10-07 12:55:51 +0400905func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400906 cName, ok := mux.Vars(r)["name"]
907 if !ok {
908 http.Error(w, "empty name", http.StatusBadRequest)
909 return
910 }
gio8c876172024-10-05 12:25:13 +0400911 tid := 0
912 if t, ok := s.tasks[cName]; ok {
913 if t.task != nil {
914 http.Error(w, "cluster task in progress", http.StatusLocked)
915 return
916 }
917 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400918 }
919 m, err := s.getClusterManager(cName)
920 if err != nil {
921 if errors.Is(err, installer.ErrorNotFound) {
922 http.Error(w, "not found", http.StatusNotFound)
923 } else {
924 http.Error(w, err.Error(), http.StatusInternalServerError)
925 }
926 return
927 }
928 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400929 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400930 go task.Start()
gio8c876172024-10-05 12:25:13 +0400931 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400932 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
933}
934
gio59946282024-10-07 12:55:51 +0400935func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400936 const vpnUser = "private-network-proxy"
937 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
938 hostname := fmt.Sprintf("cluster-%s", name)
939 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
940 app, err := installer.FindEnvApp(s.fr, "cluster-network")
941 if err != nil {
942 return installer.ReleaseResources{}, err
943 }
944 env, err := s.m.Config()
945 if err != nil {
946 return installer.ReleaseResources{}, err
947 }
gio721c0042025-04-03 11:56:36 +0400948 keys, err := installer.NewSSHKeyPair("port-allocator")
949 if err != nil {
950 return installer.ReleaseResources{}, err
951 }
952 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
953 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
954 return installer.ReleaseResources{}, err
955 }
956 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
957 return installer.ReleaseResources{}, err
958 }
giof6ad2982024-08-23 17:42:49 +0400959 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
960 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400961 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400962 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
963 "cluster": map[string]any{
964 "name": name,
965 "kubeconfig": kubeconfig,
966 "ingressClassName": ingressClassName,
967 },
968 // TODO(gio): remove hardcoded user
969 "vpnUser": vpnUser,
970 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +0400971 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +0400972 })
973 if err != nil {
974 return installer.ReleaseResources{}, err
975 }
976 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
977 go s.reconciler.Reconcile(ctx)
978 return rr, err
979 })
980 ch := make(chan error)
981 t.OnDone(func(err error) {
982 ch <- err
983 })
984 go t.Start()
985 err := <-ch
986 if err != nil {
987 return nil, err
988 }
989 for {
990 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
991 if err == nil {
992 return ip, nil
993 }
994 if errors.Is(err, installer.ErrorNotFound) {
995 time.Sleep(5 * time.Second)
996 }
997 }
998 }
999}
gio8f290322024-09-21 15:37:45 +04001000
gio59946282024-10-07 12:55:51 +04001001func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +04001002 return func(cm cluster.Manager) error {
1003 name := cm.State().Name
1004 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
1005 app, err := installer.FindEnvApp(s.fr, "longhorn")
1006 if err != nil {
1007 return installer.ReleaseResources{}, err
1008 }
1009 env, err := s.m.Config()
1010 if err != nil {
1011 return installer.ReleaseResources{}, err
1012 }
1013 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
1014 appDir := fmt.Sprintf("/clusters/%s/storage", name)
1015 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
1016 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
1017 "cluster": name,
1018 })
1019 if err != nil {
1020 return installer.ReleaseResources{}, err
1021 }
1022 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1023 go s.reconciler.Reconcile(ctx)
1024 return rr, err
1025 })
1026 ch := make(chan error)
1027 t.OnDone(func(err error) {
1028 ch <- err
1029 })
1030 go t.Start()
1031 err := <-ch
1032 if err != nil {
1033 return err
1034 }
1035 cm.EnableStorage()
1036 return nil
1037 }
1038}
gio8c876172024-10-05 12:25:13 +04001039
gio59946282024-10-07 12:55:51 +04001040func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001041 return func(err error) {
1042 if err != nil {
1043 fmt.Printf("Task %s failed: %s", name, err.Error())
1044 }
1045 s.l.Lock()
1046 defer s.l.Unlock()
1047 s.tasks[name].task = nil
1048 go func() {
1049 time.Sleep(30 * time.Second)
1050 s.l.Lock()
1051 defer s.l.Unlock()
1052 if t, ok := s.tasks[name]; ok && t.id == id {
1053 delete(s.tasks, name)
1054 }
1055 }()
1056 }
1057}