blob: 3360ff4f51823af1942b0df74e3f4e32e0039b39 [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 {
164 Id string `json:"id"`
165 SSHPrivateKey string `json:"sshPrivateKey"`
166 Config map[string]any `json:"config"`
167}
168
169func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
170 var req dodoAppInstallReq
171 // TODO(gio): validate that no internal fields are overridden by request
172 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
173 http.Error(w, err.Error(), http.StatusBadRequest)
174 return
175 }
176 clusters, err := s.m.GetClusters()
177 if err != nil {
178 http.Error(w, err.Error(), http.StatusInternalServerError)
179 return
180 }
181 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
182 var cfg bytes.Buffer
183 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
184 http.Error(w, err.Error(), http.StatusInternalServerError)
185 return
186 }
187 app, err := installer.NewDodoApp(cfg.Bytes())
188 if err != nil {
189 http.Error(w, err.Error(), http.StatusBadRequest)
190 return
191 }
192 appDir := filepath.Join("/dodo-app", req.Id)
gio721c0042025-04-03 11:56:36 +0400193 namespace := "dodo-app-testttt" // TODO(gio)
giofc441e32024-11-11 16:26:14 +0400194 if _, err := s.m.Install(app, req.Id, appDir, namespace, map[string]any{
195 "managerAddr": "", // TODO(gio)
196 "appId": req.Id,
197 "sshPrivateKey": req.SSHPrivateKey,
198 }); err != nil {
199 http.Error(w, err.Error(), http.StatusInternalServerError)
200 return
201 }
202}
203
gio59946282024-10-07 12:55:51 +0400204func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400205 env, err := s.m.Config()
206 if err != nil {
207 http.Error(w, err.Error(), http.StatusInternalServerError)
208 return
209 }
210 networks, err := s.m.CreateNetworks(env)
211 if err != nil {
212 http.Error(w, err.Error(), http.StatusInternalServerError)
213 return
214 }
215 if err := json.NewEncoder(w).Encode(networks); err != nil {
216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
218 }
219}
220
gio59946282024-10-07 12:55:51 +0400221func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400222 clusters, err := s.m.GetClusters()
223 if err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
227 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 return
230 }
231}
232
233type proxyPair struct {
234 From string `json:"from"`
235 To string `json:"to"`
236}
237
gio59946282024-10-07 12:55:51 +0400238func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400239 var req proxyPair
240 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
241 http.Error(w, err.Error(), http.StatusBadRequest)
242 return
243 }
gio721c0042025-04-03 11:56:36 +0400244 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400245 http.Error(w, err.Error(), http.StatusInternalServerError)
246 return
247 }
248}
249
gio59946282024-10-07 12:55:51 +0400250func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400251 var req proxyPair
252 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
253 http.Error(w, err.Error(), http.StatusBadRequest)
254 return
255 }
gio721c0042025-04-03 11:56:36 +0400256 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400257 http.Error(w, err.Error(), http.StatusInternalServerError)
258 return
259 }
260}
261
262type app struct {
263 Name string `json:"name"`
264 Icon template.HTML `json:"icon"`
265 ShortDescription string `json:"shortDescription"`
266 Slug string `json:"slug"`
267 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
268}
269
gio59946282024-10-07 12:55:51 +0400270func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400271 all, err := s.r.GetAll()
272 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400273 http.Error(w, err.Error(), http.StatusInternalServerError)
274 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400275 }
276 resp := make([]app, len(all))
277 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400278 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400279 }
gioaa0fcdb2024-06-10 22:19:25 +0400280 w.Header().Set("Content-Type", "application/json")
281 if err := json.NewEncoder(w).Encode(resp); err != nil {
282 http.Error(w, err.Error(), http.StatusInternalServerError)
283 return
284 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400285}
286
gio59946282024-10-07 12:55:51 +0400287func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400288 slug, ok := mux.Vars(r)["slug"]
289 if !ok {
290 http.Error(w, "empty slug", http.StatusBadRequest)
291 return
292 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400293 a, err := s.r.Find(slug)
294 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400295 http.Error(w, err.Error(), http.StatusInternalServerError)
296 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400297 }
gio7fbd4ad2024-08-27 10:06:39 +0400298 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400299 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400300 http.Error(w, err.Error(), http.StatusInternalServerError)
301 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400302 }
gioaa0fcdb2024-06-10 22:19:25 +0400303 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
304 w.Header().Set("Content-Type", "application/json")
305 if err := json.NewEncoder(w).Encode(resp); err != nil {
306 http.Error(w, err.Error(), http.StatusInternalServerError)
307 return
308 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400309}
310
gio59946282024-10-07 12:55:51 +0400311func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400312 slug, ok := mux.Vars(r)["slug"]
313 if !ok {
314 http.Error(w, "empty slug", http.StatusBadRequest)
315 return
316 }
gio7fbd4ad2024-08-27 10:06:39 +0400317 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400319 http.Error(w, err.Error(), http.StatusInternalServerError)
320 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400321 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400322 a, err := s.r.Find(instance.AppId)
323 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400324 http.Error(w, err.Error(), http.StatusInternalServerError)
325 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400326 }
gioaa0fcdb2024-06-10 22:19:25 +0400327 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
328 w.Header().Set("Content-Type", "application/json")
329 if err := json.NewEncoder(w).Encode(resp); err != nil {
330 http.Error(w, err.Error(), http.StatusInternalServerError)
331 return
332 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400333}
334
gio59946282024-10-07 12:55:51 +0400335func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400336 s.l.Lock()
337 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400338 slug, ok := mux.Vars(r)["slug"]
339 if !ok {
340 http.Error(w, "empty slug", http.StatusBadRequest)
341 return
342 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400343 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400344 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400345 http.Error(w, err.Error(), http.StatusInternalServerError)
346 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400347 }
gio3cdee592024-04-17 10:15:56 +0400348 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400349 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400350 http.Error(w, err.Error(), http.StatusInternalServerError)
351 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400352 }
gio3cdee592024-04-17 10:15:56 +0400353 env, err := s.m.Config()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400354 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400355 http.Error(w, err.Error(), http.StatusInternalServerError)
356 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400357 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400358 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400359 suffix, err := suffixGen.Generate()
360 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400361 http.Error(w, err.Error(), http.StatusInternalServerError)
362 return
gio3af43942024-04-16 08:13:50 +0400363 }
gio44f621b2024-04-29 09:44:38 +0400364 instanceId := a.Slug() + suffix
gio3cdee592024-04-17 10:15:56 +0400365 appDir := fmt.Sprintf("/apps/%s", instanceId)
366 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
gio1cd65152024-08-16 08:18:49 +0400367 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gio43b0f422024-08-21 10:40:13 +0400368 rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
369 if err == nil {
370 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
371 go s.reconciler.Reconcile(ctx)
372 }
373 return rr, err
gio1cd65152024-08-16 08:18:49 +0400374 })
gio778577f2024-04-29 09:44:38 +0400375 if _, ok := s.tasks[instanceId]; ok {
376 panic("MUST NOT REACH!")
377 }
gio8c876172024-10-05 12:25:13 +0400378 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
379 t.OnDone(s.cleanTask(instanceId, 0))
gio778577f2024-04-29 09:44:38 +0400380 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400381 if _, err := fmt.Fprintf(w, "/tasks/%s", instanceId); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400382 http.Error(w, err.Error(), http.StatusInternalServerError)
383 return
384 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400385}
386
gio59946282024-10-07 12:55:51 +0400387func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400388 s.l.Lock()
389 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400390 slug, ok := mux.Vars(r)["slug"]
391 if !ok {
392 http.Error(w, "empty slug", http.StatusBadRequest)
393 return
394 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400396 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400397 http.Error(w, err.Error(), http.StatusInternalServerError)
398 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400399 }
gio8c876172024-10-05 12:25:13 +0400400 tid := 0
401 if t, ok := s.tasks[slug]; ok {
402 if t.task != nil {
403 http.Error(w, "Update already in progress", http.StatusBadRequest)
404 return
405 }
406 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400407 }
giof8843412024-05-22 16:38:05 +0400408 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400409 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400410 http.Error(w, err.Error(), http.StatusInternalServerError)
411 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400412 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400413 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
414 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400415 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400416 t.OnDone(s.cleanTask(slug, tid))
417 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400418 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400419 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400420 http.Error(w, err.Error(), http.StatusInternalServerError)
421 return
422 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400423}
424
gio59946282024-10-07 12:55:51 +0400425func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400426 slug, ok := mux.Vars(r)["slug"]
427 if !ok {
428 http.Error(w, "empty slug", http.StatusBadRequest)
429 return
430 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400431 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400432 http.Error(w, err.Error(), http.StatusInternalServerError)
433 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400434 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400435 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
436 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400437 if _, err := fmt.Fprint(w, "/"); err != nil {
438 http.Error(w, err.Error(), http.StatusInternalServerError)
439 return
440 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400441}
442
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400443type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400444 Apps []app
445 CurrentPage string
446 SearchTarget string
447 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400448}
449
gio59946282024-10-07 12:55:51 +0400450func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400451 pageType := mux.Vars(r)["pageType"]
452 if pageType == "" {
453 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400454 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400455 searchQuery := r.FormValue("query")
456 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400457 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400458 http.Error(w, err.Error(), http.StatusInternalServerError)
459 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400460 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400461 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400462 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400463 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400464 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400465 http.Error(w, err.Error(), http.StatusInternalServerError)
466 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400467 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400468 switch pageType {
469 case "installed":
470 if len(instances) != 0 {
471 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
472 }
473 case "not-installed":
474 if len(instances) == 0 {
475 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
476 }
477 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400478 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
479 }
480 }
481 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400482 Apps: resp,
483 CurrentPage: pageType,
484 SearchTarget: pageType,
485 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400486 }
gioaa0fcdb2024-06-10 22:19:25 +0400487 if err := s.tmpl.index.Execute(w, data); err != nil {
488 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400489 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400490}
491
492type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400493 App installer.EnvApp
494 Instance *installer.AppInstanceConfig
495 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400496 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400497 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400498 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400499}
500
gio59946282024-10-07 12:55:51 +0400501func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400502 global, err := s.m.Config()
503 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400504 http.Error(w, err.Error(), http.StatusInternalServerError)
505 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400506 }
gioaa0fcdb2024-06-10 22:19:25 +0400507 slug, ok := mux.Vars(r)["slug"]
508 if !ok {
509 http.Error(w, "empty slug", http.StatusBadRequest)
510 return
511 }
gio3cdee592024-04-17 10:15:56 +0400512 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400513 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400514 http.Error(w, err.Error(), http.StatusInternalServerError)
515 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400516 }
gio7fbd4ad2024-08-27 10:06:39 +0400517 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400518 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400519 http.Error(w, err.Error(), http.StatusInternalServerError)
520 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400521 }
giocb34ad22024-07-11 08:01:13 +0400522 networks, err := s.m.CreateNetworks(global)
523 if err != nil {
524 http.Error(w, err.Error(), http.StatusInternalServerError)
525 return
526 }
giof6ad2982024-08-23 17:42:49 +0400527 clusters, err := s.m.GetClusters()
528 if err != nil {
529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400532 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400533 App: a,
534 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400535 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400536 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400537 CurrentPage: a.Name(),
538 }
gioaa0fcdb2024-06-10 22:19:25 +0400539 if err := s.tmpl.app.Execute(w, data); err != nil {
540 http.Error(w, err.Error(), http.StatusInternalServerError)
541 return
542 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400543}
544
gio59946282024-10-07 12:55:51 +0400545func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400546 s.l.Lock()
547 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400548 global, err := s.m.Config()
549 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400550 http.Error(w, err.Error(), http.StatusInternalServerError)
551 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552 }
gioaa0fcdb2024-06-10 22:19:25 +0400553 slug, ok := mux.Vars(r)["slug"]
554 if !ok {
555 http.Error(w, "empty slug", http.StatusBadRequest)
556 return
557 }
gio8c876172024-10-05 12:25:13 +0400558 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400559 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
560 return
561 }
gio8c876172024-10-05 12:25:13 +0400562 instance, err := s.m.GetInstance(slug)
563 if err != nil {
564 http.Error(w, err.Error(), http.StatusInternalServerError)
565 return
566 }
567 a, err := s.m.GetInstanceApp(instance.Id)
568 if err != nil {
569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 }
gio7fbd4ad2024-08-27 10:06:39 +0400572 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400573 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400574 http.Error(w, err.Error(), http.StatusInternalServerError)
575 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400576 }
giocb34ad22024-07-11 08:01:13 +0400577 networks, err := s.m.CreateNetworks(global)
578 if err != nil {
579 http.Error(w, err.Error(), http.StatusInternalServerError)
580 return
581 }
giof6ad2982024-08-23 17:42:49 +0400582 clusters, err := s.m.GetClusters()
583 if err != nil {
584 http.Error(w, err.Error(), http.StatusInternalServerError)
585 return
586 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400587 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400588 App: a,
gio778577f2024-04-29 09:44:38 +0400589 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400590 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400591 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400592 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400593 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400594 }
gioaa0fcdb2024-06-10 22:19:25 +0400595 if err := s.tmpl.app.Execute(w, data); err != nil {
596 http.Error(w, err.Error(), http.StatusInternalServerError)
597 return
598 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400599}
giof6ad2982024-08-23 17:42:49 +0400600
601type taskStatusData struct {
602 CurrentPage string
603 Task tasks.Task
604}
605
gio59946282024-10-07 12:55:51 +0400606func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400607 s.l.Lock()
608 defer s.l.Unlock()
609 slug, ok := mux.Vars(r)["slug"]
610 if !ok {
611 http.Error(w, "empty slug", http.StatusBadRequest)
612 return
613 }
614 t, ok := s.tasks[slug]
615 if !ok {
616 http.Error(w, "task not found", http.StatusInternalServerError)
617
618 return
619 }
gio8c876172024-10-05 12:25:13 +0400620 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400621 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
622 return
623 }
624 data := taskStatusData{
625 CurrentPage: "",
626 Task: t.task,
627 }
628 if err := s.tmpl.task.Execute(w, data); err != nil {
629 http.Error(w, err.Error(), http.StatusInternalServerError)
630 return
631 }
632}
633
634type clustersData struct {
635 CurrentPage string
636 Clusters []cluster.State
637}
638
gio59946282024-10-07 12:55:51 +0400639func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400640 clusters, err := s.m.GetClusters()
641 if err != nil {
642 http.Error(w, err.Error(), http.StatusInternalServerError)
643 return
644 }
645 data := clustersData{
646 "clusters",
647 clusters,
648 }
649 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
650 http.Error(w, err.Error(), http.StatusInternalServerError)
651 return
652 }
653}
654
655type clusterData struct {
656 CurrentPage string
657 Cluster cluster.State
658}
659
gio59946282024-10-07 12:55:51 +0400660func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400661 name, ok := mux.Vars(r)["name"]
662 if !ok {
663 http.Error(w, "empty name", http.StatusBadRequest)
664 return
665 }
666 m, err := s.getClusterManager(name)
667 if err != nil {
668 if errors.Is(err, installer.ErrorNotFound) {
669 http.Error(w, "not found", http.StatusNotFound)
670 } else {
671 http.Error(w, err.Error(), http.StatusInternalServerError)
672 }
673 return
674 }
675 data := clusterData{
676 "clusters",
677 m.State(),
678 }
679 if err := s.tmpl.cluster.Execute(w, data); err != nil {
680 http.Error(w, err.Error(), http.StatusInternalServerError)
681 return
682 }
683}
684
gio59946282024-10-07 12:55:51 +0400685func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400686 cName, ok := mux.Vars(r)["name"]
687 if !ok {
688 http.Error(w, "empty name", http.StatusBadRequest)
689 return
690 }
gio8c876172024-10-05 12:25:13 +0400691 tid := 0
692 if t, ok := s.tasks[cName]; ok {
693 if t.task != nil {
694 http.Error(w, "cluster task in progress", http.StatusLocked)
695 return
696 }
697 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400698 }
699 m, err := s.getClusterManager(cName)
700 if err != nil {
701 if errors.Is(err, installer.ErrorNotFound) {
702 http.Error(w, "not found", http.StatusNotFound)
703 } else {
704 http.Error(w, err.Error(), http.StatusInternalServerError)
705 }
706 return
707 }
708 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400709 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400710 go task.Start()
gio8c876172024-10-05 12:25:13 +0400711 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400712 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
713}
714
gio59946282024-10-07 12:55:51 +0400715func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400716 s.l.Lock()
717 defer s.l.Unlock()
718 cName, ok := mux.Vars(r)["cluster"]
719 if !ok {
720 http.Error(w, "empty name", http.StatusBadRequest)
721 return
722 }
gio8c876172024-10-05 12:25:13 +0400723 tid := 0
724 if t, ok := s.tasks[cName]; ok {
725 if t.task != nil {
726 http.Error(w, "cluster task in progress", http.StatusLocked)
727 return
728 }
729 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400730 }
731 sName, ok := mux.Vars(r)["server"]
732 if !ok {
733 http.Error(w, "empty name", http.StatusBadRequest)
734 return
735 }
736 m, err := s.getClusterManager(cName)
737 if err != nil {
738 if errors.Is(err, installer.ErrorNotFound) {
739 http.Error(w, "not found", http.StatusNotFound)
740 } else {
741 http.Error(w, err.Error(), http.StatusInternalServerError)
742 }
743 return
744 }
745 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400746 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400747 go task.Start()
gio8c876172024-10-05 12:25:13 +0400748 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400749 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
750}
751
gio59946282024-10-07 12:55:51 +0400752func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400753 clusters, err := s.m.GetClusters()
754 if err != nil {
755 return nil, err
756 }
757 var c *cluster.State
758 for _, i := range clusters {
759 if i.Name == cName {
760 c = &i
761 break
762 }
763 }
764 if c == nil {
765 return nil, installer.ErrorNotFound
766 }
767 return cluster.RestoreKubeManager(*c)
768}
769
gio59946282024-10-07 12:55:51 +0400770func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400771 s.l.Lock()
772 defer s.l.Unlock()
773 cName, ok := mux.Vars(r)["cluster"]
774 if !ok {
775 http.Error(w, "empty name", http.StatusBadRequest)
776 return
777 }
gio8c876172024-10-05 12:25:13 +0400778 tid := 0
779 if t, ok := s.tasks[cName]; ok {
780 if t.task != nil {
781 http.Error(w, "cluster task in progress", http.StatusLocked)
782 return
783 }
784 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400785 }
786 m, err := s.getClusterManager(cName)
787 if err != nil {
788 if errors.Is(err, installer.ErrorNotFound) {
789 http.Error(w, "not found", http.StatusNotFound)
790 } else {
791 http.Error(w, err.Error(), http.StatusInternalServerError)
792 }
793 return
794 }
795 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400796 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400797 if ip == nil {
798 http.Error(w, "invalid ip", http.StatusBadRequest)
799 return
800 }
801 port := 22
802 if p := r.PostFormValue("port"); p != "" {
803 port, err = strconv.Atoi(p)
804 if err != nil {
805 http.Error(w, err.Error(), http.StatusBadRequest)
806 return
807 }
808 }
809 server := cluster.Server{
810 IP: ip,
811 Port: port,
812 User: r.PostFormValue("user"),
813 Password: r.PostFormValue("password"),
814 }
815 var task tasks.Task
816 switch strings.ToLower(t) {
817 case "controller":
818 if len(m.State().Controllers) == 0 {
819 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
820 } else {
821 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
822 }
823 case "worker":
824 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
825 default:
826 http.Error(w, "invalid type", http.StatusBadRequest)
827 return
828 }
gio8c876172024-10-05 12:25:13 +0400829 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400830 go task.Start()
gio8c876172024-10-05 12:25:13 +0400831 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400832 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
833}
834
gio59946282024-10-07 12:55:51 +0400835func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400836 cName := r.PostFormValue("name")
837 if cName == "" {
838 http.Error(w, "no name", http.StatusBadRequest)
839 return
840 }
841 st := cluster.State{Name: cName}
842 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
843 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
844 return "", err
845 }
846 return fmt.Sprintf("create cluster: %s", cName), nil
847 }); err != nil {
848 http.Error(w, err.Error(), http.StatusInternalServerError)
849 return
850 }
851 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
852}
853
gio59946282024-10-07 12:55:51 +0400854func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400855 cName, ok := mux.Vars(r)["name"]
856 if !ok {
857 http.Error(w, "empty name", http.StatusBadRequest)
858 return
859 }
gio8c876172024-10-05 12:25:13 +0400860 tid := 0
861 if t, ok := s.tasks[cName]; ok {
862 if t.task != nil {
863 http.Error(w, "cluster task in progress", http.StatusLocked)
864 return
865 }
866 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400867 }
868 m, err := s.getClusterManager(cName)
869 if err != nil {
870 if errors.Is(err, installer.ErrorNotFound) {
871 http.Error(w, "not found", http.StatusNotFound)
872 } else {
873 http.Error(w, err.Error(), http.StatusInternalServerError)
874 }
875 return
876 }
877 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400878 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400879 go task.Start()
gio8c876172024-10-05 12:25:13 +0400880 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400881 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
882}
883
gio59946282024-10-07 12:55:51 +0400884func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400885 const vpnUser = "private-network-proxy"
886 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
887 hostname := fmt.Sprintf("cluster-%s", name)
888 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
889 app, err := installer.FindEnvApp(s.fr, "cluster-network")
890 if err != nil {
891 return installer.ReleaseResources{}, err
892 }
893 env, err := s.m.Config()
894 if err != nil {
895 return installer.ReleaseResources{}, err
896 }
gio721c0042025-04-03 11:56:36 +0400897 keys, err := installer.NewSSHKeyPair("port-allocator")
898 if err != nil {
899 return installer.ReleaseResources{}, err
900 }
901 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
902 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
903 return installer.ReleaseResources{}, err
904 }
905 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
906 return installer.ReleaseResources{}, err
907 }
giof6ad2982024-08-23 17:42:49 +0400908 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
909 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400910 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400911 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
912 "cluster": map[string]any{
913 "name": name,
914 "kubeconfig": kubeconfig,
915 "ingressClassName": ingressClassName,
916 },
917 // TODO(gio): remove hardcoded user
918 "vpnUser": vpnUser,
919 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +0400920 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +0400921 })
922 if err != nil {
923 return installer.ReleaseResources{}, err
924 }
925 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
926 go s.reconciler.Reconcile(ctx)
927 return rr, err
928 })
929 ch := make(chan error)
930 t.OnDone(func(err error) {
931 ch <- err
932 })
933 go t.Start()
934 err := <-ch
935 if err != nil {
936 return nil, err
937 }
938 for {
939 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
940 if err == nil {
941 return ip, nil
942 }
943 if errors.Is(err, installer.ErrorNotFound) {
944 time.Sleep(5 * time.Second)
945 }
946 }
947 }
948}
gio8f290322024-09-21 15:37:45 +0400949
gio59946282024-10-07 12:55:51 +0400950func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +0400951 return func(cm cluster.Manager) error {
952 name := cm.State().Name
953 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
954 app, err := installer.FindEnvApp(s.fr, "longhorn")
955 if err != nil {
956 return installer.ReleaseResources{}, err
957 }
958 env, err := s.m.Config()
959 if err != nil {
960 return installer.ReleaseResources{}, err
961 }
962 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
963 appDir := fmt.Sprintf("/clusters/%s/storage", name)
964 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
965 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
966 "cluster": name,
967 })
968 if err != nil {
969 return installer.ReleaseResources{}, err
970 }
971 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
972 go s.reconciler.Reconcile(ctx)
973 return rr, err
974 })
975 ch := make(chan error)
976 t.OnDone(func(err error) {
977 ch <- err
978 })
979 go t.Start()
980 err := <-ch
981 if err != nil {
982 return err
983 }
984 cm.EnableStorage()
985 return nil
986 }
987}
gio8c876172024-10-05 12:25:13 +0400988
gio59946282024-10-07 12:55:51 +0400989func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +0400990 return func(err error) {
991 if err != nil {
992 fmt.Printf("Task %s failed: %s", name, err.Error())
993 }
994 s.l.Lock()
995 defer s.l.Unlock()
996 s.tasks[name].task = nil
997 go func() {
998 time.Sleep(30 * time.Second)
999 s.l.Lock()
1000 defer s.l.Unlock()
1001 if t, ok := s.tasks[name]; ok && t.id == id {
1002 delete(s.tasks, name)
1003 }
1004 }()
1005 }
1006}