blob: f4b543b317965b4414fdc634b21149d29c784f11 [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"
giofc441e32024-11-11 16:26:14 +040013 "path/filepath"
giof6ad2982024-08-23 17:42:49 +040014 "strconv"
15 "strings"
16 "sync"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040017 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040018
19 "github.com/Masterminds/sprig/v3"
gioaa0fcdb2024-06-10 22:19:25 +040020 "github.com/gorilla/mux"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040021
22 "github.com/giolekva/pcloud/core/installer"
giof6ad2982024-08-23 17:42:49 +040023 "github.com/giolekva/pcloud/core/installer/cluster"
gio59946282024-10-07 12:55:51 +040024 "github.com/giolekva/pcloud/core/installer/server"
giof6ad2982024-08-23 17:42:49 +040025 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040026 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040027)
28
gio59946282024-10-07 12:55:51 +040029//go:embed templates/*
30var templates embed.FS
31
32//go:embed static/*
33var staticAssets embed.FS
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040034
giof6ad2982024-08-23 17:42:49 +040035type taskForward struct {
36 task tasks.Task
37 redirectTo string
gio8c876172024-10-05 12:25:13 +040038 id int
giof6ad2982024-08-23 17:42:49 +040039}
40
gio59946282024-10-07 12:55:51 +040041type Server struct {
giof6ad2982024-08-23 17:42:49 +040042 l sync.Locker
43 port int
gio721c0042025-04-03 11:56:36 +040044 ssClient soft.Client
giof6ad2982024-08-23 17:42:49 +040045 repo soft.RepoIO
46 m *installer.AppManager
47 r installer.AppRepository
48 fr installer.AppRepository
49 reconciler *tasks.FixedReconciler
50 h installer.HelmReleaseMonitor
51 cnc installer.ClusterNetworkConfigurator
52 vpnAPIClient installer.VPNAPIClient
gio8c876172024-10-05 12:25:13 +040053 tasks map[string]*taskForward
giof6ad2982024-08-23 17:42:49 +040054 tmpl tmplts
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040055}
56
57type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040058 index *template.Template
59 app *template.Template
60 allClusters *template.Template
61 cluster *template.Template
62 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040063}
64
gio59946282024-10-07 12:55:51 +040065func parseTemplates(fs embed.FS) (tmplts, error) {
66 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "templates/base.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040067 if err != nil {
68 return tmplts{}, err
69 }
70 parse := func(path string) (*template.Template, error) {
71 if b, err := base.Clone(); err != nil {
72 return nil, err
73 } else {
74 return b.ParseFS(fs, path)
75 }
76 }
gio59946282024-10-07 12:55:51 +040077 index, err := parse("templates/index.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040078 if err != nil {
79 return tmplts{}, err
80 }
gio59946282024-10-07 12:55:51 +040081 app, err := parse("templates/app.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040082 if err != nil {
83 return tmplts{}, err
84 }
gio59946282024-10-07 12:55:51 +040085 allClusters, err := parse("templates/all-clusters.html")
giof6ad2982024-08-23 17:42:49 +040086 if err != nil {
87 return tmplts{}, err
88 }
gio59946282024-10-07 12:55:51 +040089 cluster, err := parse("templates/cluster.html")
giof6ad2982024-08-23 17:42:49 +040090 if err != nil {
91 return tmplts{}, err
92 }
gio59946282024-10-07 12:55:51 +040093 task, err := parse("templates/task.html")
giof6ad2982024-08-23 17:42:49 +040094 if err != nil {
95 return tmplts{}, err
96 }
97 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040098}
99
gio59946282024-10-07 12:55:51 +0400100func NewServer(
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400101 port int,
gio721c0042025-04-03 11:56:36 +0400102 ssClient soft.Client,
giof6ad2982024-08-23 17:42:49 +0400103 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400104 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400105 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400106 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400107 reconciler *tasks.FixedReconciler,
gio778577f2024-04-29 09:44:38 +0400108 h installer.HelmReleaseMonitor,
giof6ad2982024-08-23 17:42:49 +0400109 cnc installer.ClusterNetworkConfigurator,
110 vpnAPIClient installer.VPNAPIClient,
gio59946282024-10-07 12:55:51 +0400111) (*Server, error) {
112 tmpl, err := parseTemplates(templates)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400113 if err != nil {
114 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400115 }
gio59946282024-10-07 12:55:51 +0400116 return &Server{
giof6ad2982024-08-23 17:42:49 +0400117 l: &sync.Mutex{},
118 port: port,
gio721c0042025-04-03 11:56:36 +0400119 ssClient: ssClient,
giof6ad2982024-08-23 17:42:49 +0400120 repo: repo,
121 m: m,
122 r: r,
123 fr: fr,
124 reconciler: reconciler,
125 h: h,
126 cnc: cnc,
127 vpnAPIClient: vpnAPIClient,
gio8c876172024-10-05 12:25:13 +0400128 tasks: make(map[string]*taskForward),
giof6ad2982024-08-23 17:42:49 +0400129 tmpl: tmpl,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400130 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400131}
132
gio59946282024-10-07 12:55:51 +0400133func (s *Server) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400134 r := mux.NewRouter()
gio59946282024-10-07 12:55:51 +0400135 r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
giocb34ad22024-07-11 08:01:13 +0400136 r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
giof15b9da2024-09-19 06:59:16 +0400137 r.HandleFunc("/api/clusters", s.handleClusters).Methods(http.MethodGet)
138 r.HandleFunc("/api/proxy/add", s.handleProxyAdd).Methods(http.MethodPost)
139 r.HandleFunc("/api/proxy/remove", s.handleProxyRemove).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400140 r.HandleFunc("/api/app-repo", s.handleAppRepo)
141 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
142 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
143 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
144 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
145 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
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 Id string `json:"id"`
165 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400166}
167
168func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
169 var req dodoAppInstallReq
170 // TODO(gio): validate that no internal fields are overridden by request
171 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
172 http.Error(w, err.Error(), http.StatusBadRequest)
173 return
174 }
175 clusters, err := s.m.GetClusters()
176 if err != nil {
177 http.Error(w, err.Error(), http.StatusInternalServerError)
178 return
179 }
180 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
181 var cfg bytes.Buffer
182 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 }
186 app, err := installer.NewDodoApp(cfg.Bytes())
187 if err != nil {
188 http.Error(w, err.Error(), http.StatusBadRequest)
189 return
190 }
191 appDir := filepath.Join("/dodo-app", req.Id)
gio721c0042025-04-03 11:56:36 +0400192 namespace := "dodo-app-testttt" // TODO(gio)
giofc441e32024-11-11 16:26:14 +0400193 if _, err := s.m.Install(app, req.Id, appDir, namespace, map[string]any{
gio74e73e92025-04-20 11:57:44 +0400194 "managerAddr": "", // TODO(gio)
195 "appId": req.Id,
giofc441e32024-11-11 16:26:14 +0400196 }); err != nil {
197 http.Error(w, err.Error(), http.StatusInternalServerError)
198 return
199 }
200}
201
gio59946282024-10-07 12:55:51 +0400202func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400203 env, err := s.m.Config()
204 if err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 }
208 networks, err := s.m.CreateNetworks(env)
209 if err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 }
213 if err := json.NewEncoder(w).Encode(networks); err != nil {
214 http.Error(w, err.Error(), http.StatusInternalServerError)
215 return
216 }
217}
218
gio59946282024-10-07 12:55:51 +0400219func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400220 clusters, err := s.m.GetClusters()
221 if err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
224 }
225 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
226 http.Error(w, err.Error(), http.StatusInternalServerError)
227 return
228 }
229}
230
231type proxyPair struct {
232 From string `json:"from"`
233 To string `json:"to"`
234}
235
gio59946282024-10-07 12:55:51 +0400236func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400237 var req proxyPair
238 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
239 http.Error(w, err.Error(), http.StatusBadRequest)
240 return
241 }
gio721c0042025-04-03 11:56:36 +0400242 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 return
245 }
246}
247
gio59946282024-10-07 12:55:51 +0400248func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400249 var req proxyPair
250 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
251 http.Error(w, err.Error(), http.StatusBadRequest)
252 return
253 }
gio721c0042025-04-03 11:56:36 +0400254 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
257 }
258}
259
260type app struct {
261 Name string `json:"name"`
262 Icon template.HTML `json:"icon"`
263 ShortDescription string `json:"shortDescription"`
264 Slug string `json:"slug"`
265 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
266}
267
gio59946282024-10-07 12:55:51 +0400268func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400269 all, err := s.r.GetAll()
270 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400271 http.Error(w, err.Error(), http.StatusInternalServerError)
272 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400273 }
274 resp := make([]app, len(all))
275 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400276 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400277 }
gioaa0fcdb2024-06-10 22:19:25 +0400278 w.Header().Set("Content-Type", "application/json")
279 if err := json.NewEncoder(w).Encode(resp); err != nil {
280 http.Error(w, err.Error(), http.StatusInternalServerError)
281 return
282 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400283}
284
gio59946282024-10-07 12:55:51 +0400285func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400286 slug, ok := mux.Vars(r)["slug"]
287 if !ok {
288 http.Error(w, "empty slug", http.StatusBadRequest)
289 return
290 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400291 a, err := s.r.Find(slug)
292 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400293 http.Error(w, err.Error(), http.StatusInternalServerError)
294 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400295 }
gio7fbd4ad2024-08-27 10:06:39 +0400296 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400297 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400298 http.Error(w, err.Error(), http.StatusInternalServerError)
299 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400300 }
gioaa0fcdb2024-06-10 22:19:25 +0400301 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
302 w.Header().Set("Content-Type", "application/json")
303 if err := json.NewEncoder(w).Encode(resp); err != nil {
304 http.Error(w, err.Error(), http.StatusInternalServerError)
305 return
306 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400307}
308
gio59946282024-10-07 12:55:51 +0400309func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400310 slug, ok := mux.Vars(r)["slug"]
311 if !ok {
312 http.Error(w, "empty slug", http.StatusBadRequest)
313 return
314 }
gio7fbd4ad2024-08-27 10:06:39 +0400315 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400316 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400317 http.Error(w, err.Error(), http.StatusInternalServerError)
318 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400319 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400320 a, err := s.r.Find(instance.AppId)
321 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400322 http.Error(w, err.Error(), http.StatusInternalServerError)
323 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400324 }
gioaa0fcdb2024-06-10 22:19:25 +0400325 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
326 w.Header().Set("Content-Type", "application/json")
327 if err := json.NewEncoder(w).Encode(resp); err != nil {
328 http.Error(w, err.Error(), http.StatusInternalServerError)
329 return
330 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400331}
332
gio59946282024-10-07 12:55:51 +0400333func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400334 s.l.Lock()
335 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400336 slug, ok := mux.Vars(r)["slug"]
337 if !ok {
338 http.Error(w, "empty slug", http.StatusBadRequest)
339 return
340 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400341 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400342 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400343 http.Error(w, err.Error(), http.StatusInternalServerError)
344 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400345 }
gio3cdee592024-04-17 10:15:56 +0400346 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400347 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400348 http.Error(w, err.Error(), http.StatusInternalServerError)
349 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400350 }
gio3cdee592024-04-17 10:15:56 +0400351 env, err := s.m.Config()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400352 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400353 http.Error(w, err.Error(), http.StatusInternalServerError)
354 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400355 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400356 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400357 suffix, err := suffixGen.Generate()
358 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
gio3af43942024-04-16 08:13:50 +0400361 }
gio44f621b2024-04-29 09:44:38 +0400362 instanceId := a.Slug() + suffix
gio3cdee592024-04-17 10:15:56 +0400363 appDir := fmt.Sprintf("/apps/%s", instanceId)
364 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
gio1cd65152024-08-16 08:18:49 +0400365 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gio43b0f422024-08-21 10:40:13 +0400366 rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
367 if err == nil {
368 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
369 go s.reconciler.Reconcile(ctx)
370 }
371 return rr, err
gio1cd65152024-08-16 08:18:49 +0400372 })
gio778577f2024-04-29 09:44:38 +0400373 if _, ok := s.tasks[instanceId]; ok {
374 panic("MUST NOT REACH!")
375 }
gio8c876172024-10-05 12:25:13 +0400376 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
377 t.OnDone(s.cleanTask(instanceId, 0))
gio778577f2024-04-29 09:44:38 +0400378 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400379 if _, err := fmt.Fprintf(w, "/tasks/%s", instanceId); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400380 http.Error(w, err.Error(), http.StatusInternalServerError)
381 return
382 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400383}
384
gio59946282024-10-07 12:55:51 +0400385func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400386 s.l.Lock()
387 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400388 slug, ok := mux.Vars(r)["slug"]
389 if !ok {
390 http.Error(w, "empty slug", http.StatusBadRequest)
391 return
392 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400393 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400394 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400397 }
gio8c876172024-10-05 12:25:13 +0400398 tid := 0
399 if t, ok := s.tasks[slug]; ok {
400 if t.task != nil {
401 http.Error(w, "Update already in progress", http.StatusBadRequest)
402 return
403 }
404 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400405 }
giof8843412024-05-22 16:38:05 +0400406 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400407 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400408 http.Error(w, err.Error(), http.StatusInternalServerError)
409 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400410 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400411 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
412 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400413 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400414 t.OnDone(s.cleanTask(slug, tid))
415 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400416 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400417 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400421}
422
gio59946282024-10-07 12:55:51 +0400423func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400424 slug, ok := mux.Vars(r)["slug"]
425 if !ok {
426 http.Error(w, "empty slug", http.StatusBadRequest)
427 return
428 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400429 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400430 http.Error(w, err.Error(), http.StatusInternalServerError)
431 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400432 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400433 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
434 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400435 if _, err := fmt.Fprint(w, "/"); err != nil {
436 http.Error(w, err.Error(), http.StatusInternalServerError)
437 return
438 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400439}
440
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400441type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400442 Apps []app
443 CurrentPage string
444 SearchTarget string
445 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400446}
447
gio59946282024-10-07 12:55:51 +0400448func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400449 pageType := mux.Vars(r)["pageType"]
450 if pageType == "" {
451 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400452 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400453 searchQuery := r.FormValue("query")
454 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400455 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400456 http.Error(w, err.Error(), http.StatusInternalServerError)
457 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400458 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400459 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400460 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400461 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400462 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400463 http.Error(w, err.Error(), http.StatusInternalServerError)
464 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400465 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400466 switch pageType {
467 case "installed":
468 if len(instances) != 0 {
469 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
470 }
471 case "not-installed":
472 if len(instances) == 0 {
473 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
474 }
475 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400476 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
477 }
478 }
479 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400480 Apps: resp,
481 CurrentPage: pageType,
482 SearchTarget: pageType,
483 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400484 }
gioaa0fcdb2024-06-10 22:19:25 +0400485 if err := s.tmpl.index.Execute(w, data); err != nil {
486 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400487 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400488}
489
490type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400491 App installer.EnvApp
492 Instance *installer.AppInstanceConfig
493 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400494 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400495 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400496 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400497}
498
gio59946282024-10-07 12:55:51 +0400499func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400500 global, err := s.m.Config()
501 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400502 http.Error(w, err.Error(), http.StatusInternalServerError)
503 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400504 }
gioaa0fcdb2024-06-10 22:19:25 +0400505 slug, ok := mux.Vars(r)["slug"]
506 if !ok {
507 http.Error(w, "empty slug", http.StatusBadRequest)
508 return
509 }
gio3cdee592024-04-17 10:15:56 +0400510 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400511 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400514 }
gio7fbd4ad2024-08-27 10:06:39 +0400515 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400516 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400519 }
giocb34ad22024-07-11 08:01:13 +0400520 networks, err := s.m.CreateNetworks(global)
521 if err != nil {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
524 }
giof6ad2982024-08-23 17:42:49 +0400525 clusters, err := s.m.GetClusters()
526 if err != nil {
527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
529 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400530 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400531 App: a,
532 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400533 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400534 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400535 CurrentPage: a.Name(),
536 }
gioaa0fcdb2024-06-10 22:19:25 +0400537 if err := s.tmpl.app.Execute(w, data); err != nil {
538 http.Error(w, err.Error(), http.StatusInternalServerError)
539 return
540 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400541}
542
gio59946282024-10-07 12:55:51 +0400543func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400544 s.l.Lock()
545 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400546 global, err := s.m.Config()
547 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400550 }
gioaa0fcdb2024-06-10 22:19:25 +0400551 slug, ok := mux.Vars(r)["slug"]
552 if !ok {
553 http.Error(w, "empty slug", http.StatusBadRequest)
554 return
555 }
gio8c876172024-10-05 12:25:13 +0400556 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400557 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
558 return
559 }
gio8c876172024-10-05 12:25:13 +0400560 instance, err := s.m.GetInstance(slug)
561 if err != nil {
562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 }
565 a, err := s.m.GetInstanceApp(instance.Id)
566 if err != nil {
567 http.Error(w, err.Error(), http.StatusInternalServerError)
568 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400569 }
gio7fbd4ad2024-08-27 10:06:39 +0400570 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400572 http.Error(w, err.Error(), http.StatusInternalServerError)
573 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400574 }
giocb34ad22024-07-11 08:01:13 +0400575 networks, err := s.m.CreateNetworks(global)
576 if err != nil {
577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
giof6ad2982024-08-23 17:42:49 +0400580 clusters, err := s.m.GetClusters()
581 if err != nil {
582 http.Error(w, err.Error(), http.StatusInternalServerError)
583 return
584 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400585 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400586 App: a,
gio778577f2024-04-29 09:44:38 +0400587 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400588 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400589 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400590 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400591 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400592 }
gioaa0fcdb2024-06-10 22:19:25 +0400593 if err := s.tmpl.app.Execute(w, data); err != nil {
594 http.Error(w, err.Error(), http.StatusInternalServerError)
595 return
596 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400597}
giof6ad2982024-08-23 17:42:49 +0400598
599type taskStatusData struct {
600 CurrentPage string
601 Task tasks.Task
602}
603
gio59946282024-10-07 12:55:51 +0400604func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400605 s.l.Lock()
606 defer s.l.Unlock()
607 slug, ok := mux.Vars(r)["slug"]
608 if !ok {
609 http.Error(w, "empty slug", http.StatusBadRequest)
610 return
611 }
612 t, ok := s.tasks[slug]
613 if !ok {
614 http.Error(w, "task not found", http.StatusInternalServerError)
615
616 return
617 }
gio8c876172024-10-05 12:25:13 +0400618 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400619 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
620 return
621 }
622 data := taskStatusData{
623 CurrentPage: "",
624 Task: t.task,
625 }
626 if err := s.tmpl.task.Execute(w, data); err != nil {
627 http.Error(w, err.Error(), http.StatusInternalServerError)
628 return
629 }
630}
631
632type clustersData struct {
633 CurrentPage string
634 Clusters []cluster.State
635}
636
gio59946282024-10-07 12:55:51 +0400637func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400638 clusters, err := s.m.GetClusters()
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
643 data := clustersData{
644 "clusters",
645 clusters,
646 }
647 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
651}
652
653type clusterData struct {
654 CurrentPage string
655 Cluster cluster.State
656}
657
gio59946282024-10-07 12:55:51 +0400658func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400659 name, ok := mux.Vars(r)["name"]
660 if !ok {
661 http.Error(w, "empty name", http.StatusBadRequest)
662 return
663 }
664 m, err := s.getClusterManager(name)
665 if err != nil {
666 if errors.Is(err, installer.ErrorNotFound) {
667 http.Error(w, "not found", http.StatusNotFound)
668 } else {
669 http.Error(w, err.Error(), http.StatusInternalServerError)
670 }
671 return
672 }
673 data := clusterData{
674 "clusters",
675 m.State(),
676 }
677 if err := s.tmpl.cluster.Execute(w, data); err != nil {
678 http.Error(w, err.Error(), http.StatusInternalServerError)
679 return
680 }
681}
682
gio59946282024-10-07 12:55:51 +0400683func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400684 cName, ok := mux.Vars(r)["name"]
685 if !ok {
686 http.Error(w, "empty name", http.StatusBadRequest)
687 return
688 }
gio8c876172024-10-05 12:25:13 +0400689 tid := 0
690 if t, ok := s.tasks[cName]; ok {
691 if t.task != nil {
692 http.Error(w, "cluster task in progress", http.StatusLocked)
693 return
694 }
695 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400696 }
697 m, err := s.getClusterManager(cName)
698 if err != nil {
699 if errors.Is(err, installer.ErrorNotFound) {
700 http.Error(w, "not found", http.StatusNotFound)
701 } else {
702 http.Error(w, err.Error(), http.StatusInternalServerError)
703 }
704 return
705 }
706 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400707 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400708 go task.Start()
gio8c876172024-10-05 12:25:13 +0400709 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400710 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
711}
712
gio59946282024-10-07 12:55:51 +0400713func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400714 s.l.Lock()
715 defer s.l.Unlock()
716 cName, ok := mux.Vars(r)["cluster"]
717 if !ok {
718 http.Error(w, "empty name", http.StatusBadRequest)
719 return
720 }
gio8c876172024-10-05 12:25:13 +0400721 tid := 0
722 if t, ok := s.tasks[cName]; ok {
723 if t.task != nil {
724 http.Error(w, "cluster task in progress", http.StatusLocked)
725 return
726 }
727 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400728 }
729 sName, ok := mux.Vars(r)["server"]
730 if !ok {
731 http.Error(w, "empty name", http.StatusBadRequest)
732 return
733 }
734 m, err := s.getClusterManager(cName)
735 if err != nil {
736 if errors.Is(err, installer.ErrorNotFound) {
737 http.Error(w, "not found", http.StatusNotFound)
738 } else {
739 http.Error(w, err.Error(), http.StatusInternalServerError)
740 }
741 return
742 }
743 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400744 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400745 go task.Start()
gio8c876172024-10-05 12:25:13 +0400746 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400747 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
748}
749
gio59946282024-10-07 12:55:51 +0400750func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400751 clusters, err := s.m.GetClusters()
752 if err != nil {
753 return nil, err
754 }
755 var c *cluster.State
756 for _, i := range clusters {
757 if i.Name == cName {
758 c = &i
759 break
760 }
761 }
762 if c == nil {
763 return nil, installer.ErrorNotFound
764 }
765 return cluster.RestoreKubeManager(*c)
766}
767
gio59946282024-10-07 12:55:51 +0400768func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400769 s.l.Lock()
770 defer s.l.Unlock()
771 cName, ok := mux.Vars(r)["cluster"]
772 if !ok {
773 http.Error(w, "empty name", http.StatusBadRequest)
774 return
775 }
gio8c876172024-10-05 12:25:13 +0400776 tid := 0
777 if t, ok := s.tasks[cName]; ok {
778 if t.task != nil {
779 http.Error(w, "cluster task in progress", http.StatusLocked)
780 return
781 }
782 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400783 }
784 m, err := s.getClusterManager(cName)
785 if err != nil {
786 if errors.Is(err, installer.ErrorNotFound) {
787 http.Error(w, "not found", http.StatusNotFound)
788 } else {
789 http.Error(w, err.Error(), http.StatusInternalServerError)
790 }
791 return
792 }
793 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400794 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400795 if ip == nil {
796 http.Error(w, "invalid ip", http.StatusBadRequest)
797 return
798 }
799 port := 22
800 if p := r.PostFormValue("port"); p != "" {
801 port, err = strconv.Atoi(p)
802 if err != nil {
803 http.Error(w, err.Error(), http.StatusBadRequest)
804 return
805 }
806 }
807 server := cluster.Server{
808 IP: ip,
809 Port: port,
810 User: r.PostFormValue("user"),
811 Password: r.PostFormValue("password"),
812 }
813 var task tasks.Task
814 switch strings.ToLower(t) {
815 case "controller":
816 if len(m.State().Controllers) == 0 {
817 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
818 } else {
819 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
820 }
821 case "worker":
822 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
823 default:
824 http.Error(w, "invalid type", http.StatusBadRequest)
825 return
826 }
gio8c876172024-10-05 12:25:13 +0400827 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400828 go task.Start()
gio8c876172024-10-05 12:25:13 +0400829 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400830 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
831}
832
gio59946282024-10-07 12:55:51 +0400833func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400834 cName := r.PostFormValue("name")
835 if cName == "" {
836 http.Error(w, "no name", http.StatusBadRequest)
837 return
838 }
839 st := cluster.State{Name: cName}
840 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
841 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
842 return "", err
843 }
844 return fmt.Sprintf("create cluster: %s", cName), nil
845 }); err != nil {
846 http.Error(w, err.Error(), http.StatusInternalServerError)
847 return
848 }
849 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
850}
851
gio59946282024-10-07 12:55:51 +0400852func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400853 cName, ok := mux.Vars(r)["name"]
854 if !ok {
855 http.Error(w, "empty name", http.StatusBadRequest)
856 return
857 }
gio8c876172024-10-05 12:25:13 +0400858 tid := 0
859 if t, ok := s.tasks[cName]; ok {
860 if t.task != nil {
861 http.Error(w, "cluster task in progress", http.StatusLocked)
862 return
863 }
864 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400865 }
866 m, err := s.getClusterManager(cName)
867 if err != nil {
868 if errors.Is(err, installer.ErrorNotFound) {
869 http.Error(w, "not found", http.StatusNotFound)
870 } else {
871 http.Error(w, err.Error(), http.StatusInternalServerError)
872 }
873 return
874 }
875 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400876 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400877 go task.Start()
gio8c876172024-10-05 12:25:13 +0400878 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400879 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
880}
881
gio59946282024-10-07 12:55:51 +0400882func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400883 const vpnUser = "private-network-proxy"
884 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
885 hostname := fmt.Sprintf("cluster-%s", name)
886 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
887 app, err := installer.FindEnvApp(s.fr, "cluster-network")
888 if err != nil {
889 return installer.ReleaseResources{}, err
890 }
891 env, err := s.m.Config()
892 if err != nil {
893 return installer.ReleaseResources{}, err
894 }
gio721c0042025-04-03 11:56:36 +0400895 keys, err := installer.NewSSHKeyPair("port-allocator")
896 if err != nil {
897 return installer.ReleaseResources{}, err
898 }
899 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
900 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
901 return installer.ReleaseResources{}, err
902 }
903 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
904 return installer.ReleaseResources{}, err
905 }
giof6ad2982024-08-23 17:42:49 +0400906 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
907 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400908 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400909 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
910 "cluster": map[string]any{
911 "name": name,
912 "kubeconfig": kubeconfig,
913 "ingressClassName": ingressClassName,
914 },
915 // TODO(gio): remove hardcoded user
916 "vpnUser": vpnUser,
917 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +0400918 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +0400919 })
920 if err != nil {
921 return installer.ReleaseResources{}, err
922 }
923 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
924 go s.reconciler.Reconcile(ctx)
925 return rr, err
926 })
927 ch := make(chan error)
928 t.OnDone(func(err error) {
929 ch <- err
930 })
931 go t.Start()
932 err := <-ch
933 if err != nil {
934 return nil, err
935 }
936 for {
937 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
938 if err == nil {
939 return ip, nil
940 }
941 if errors.Is(err, installer.ErrorNotFound) {
942 time.Sleep(5 * time.Second)
943 }
944 }
945 }
946}
gio8f290322024-09-21 15:37:45 +0400947
gio59946282024-10-07 12:55:51 +0400948func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +0400949 return func(cm cluster.Manager) error {
950 name := cm.State().Name
951 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
952 app, err := installer.FindEnvApp(s.fr, "longhorn")
953 if err != nil {
954 return installer.ReleaseResources{}, err
955 }
956 env, err := s.m.Config()
957 if err != nil {
958 return installer.ReleaseResources{}, err
959 }
960 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
961 appDir := fmt.Sprintf("/clusters/%s/storage", name)
962 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
963 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
964 "cluster": name,
965 })
966 if err != nil {
967 return installer.ReleaseResources{}, err
968 }
969 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
970 go s.reconciler.Reconcile(ctx)
971 return rr, err
972 })
973 ch := make(chan error)
974 t.OnDone(func(err error) {
975 ch <- err
976 })
977 go t.Start()
978 err := <-ch
979 if err != nil {
980 return err
981 }
982 cm.EnableStorage()
983 return nil
984 }
985}
gio8c876172024-10-05 12:25:13 +0400986
gio59946282024-10-07 12:55:51 +0400987func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +0400988 return func(err error) {
989 if err != nil {
990 fmt.Printf("Task %s failed: %s", name, err.Error())
991 }
992 s.l.Lock()
993 defer s.l.Unlock()
994 s.tasks[name].task = nil
995 go func() {
996 time.Sleep(30 * time.Second)
997 s.l.Lock()
998 defer s.l.Unlock()
999 if t, ok := s.tasks[name]; ok && t.id == id {
1000 delete(s.tasks, name)
1001 }
1002 }()
1003 }
1004}