blob: 76212135dc2d912bf05fa24ba63a901d612784d2 [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)
giofc441e32024-11-11 16:26:14 +0400145 r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400146 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
147 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
148 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400149 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400150 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
151 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
152 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400153 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
154 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400155 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400156 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
157 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400158 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400159 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400160}
161
giofc441e32024-11-11 16:26:14 +0400162type dodoAppInstallReq struct {
gio74e73e92025-04-20 11:57:44 +0400163 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400164}
165
gio218e8132025-04-22 17:11:58 +0000166type dodoAppInstallResp struct {
167 Id string `json:"id"`
168 DeployKey string `json:"deployKey"`
169}
170
171type dodoAppRendered struct {
172 Input struct {
173 Key struct {
174 Public string `json:"public"`
175 } `json:"key"`
176 } `json:"input"`
177}
178
giofc441e32024-11-11 16:26:14 +0400179func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
180 var req dodoAppInstallReq
181 // TODO(gio): validate that no internal fields are overridden by request
182 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
183 http.Error(w, err.Error(), http.StatusBadRequest)
184 return
185 }
186 clusters, err := s.m.GetClusters()
187 if err != nil {
188 http.Error(w, err.Error(), http.StatusInternalServerError)
189 return
190 }
191 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
192 var cfg bytes.Buffer
193 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
194 http.Error(w, err.Error(), http.StatusInternalServerError)
195 return
196 }
197 app, err := installer.NewDodoApp(cfg.Bytes())
198 if err != nil {
199 http.Error(w, err.Error(), http.StatusBadRequest)
200 return
201 }
gio218e8132025-04-22 17:11:58 +0000202 if instanceId, rr, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400203 http.Error(w, err.Error(), http.StatusInternalServerError)
204 return
gioa421b062025-04-21 09:45:04 +0400205 } else {
gio218e8132025-04-22 17:11:58 +0000206 var cfg dodoAppRendered
207 if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 }
210 if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
211 Id: instanceId,
212 DeployKey: cfg.Input.Key.Public,
213 }); err != nil {
214 http.Error(w, err.Error(), http.StatusInternalServerError)
215 }
giofc441e32024-11-11 16:26:14 +0400216 }
217}
218
gio59946282024-10-07 12:55:51 +0400219func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400220 env, err := s.m.Config()
221 if err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
224 }
225 networks, err := s.m.CreateNetworks(env)
226 if err != nil {
227 http.Error(w, err.Error(), http.StatusInternalServerError)
228 return
229 }
230 if err := json.NewEncoder(w).Encode(networks); err != nil {
231 http.Error(w, err.Error(), http.StatusInternalServerError)
232 return
233 }
234}
235
gio59946282024-10-07 12:55:51 +0400236func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400237 clusters, err := s.m.GetClusters()
238 if err != nil {
239 http.Error(w, err.Error(), http.StatusInternalServerError)
240 return
241 }
242 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 return
245 }
246}
247
248type proxyPair struct {
249 From string `json:"from"`
250 To string `json:"to"`
251}
252
gio59946282024-10-07 12:55:51 +0400253func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400254 var req proxyPair
255 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
256 http.Error(w, err.Error(), http.StatusBadRequest)
257 return
258 }
gio721c0042025-04-03 11:56:36 +0400259 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400260 http.Error(w, err.Error(), http.StatusInternalServerError)
261 return
262 }
263}
264
gio59946282024-10-07 12:55:51 +0400265func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400266 var req proxyPair
267 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
268 http.Error(w, err.Error(), http.StatusBadRequest)
269 return
270 }
gio721c0042025-04-03 11:56:36 +0400271 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
274 }
275}
276
277type app struct {
278 Name string `json:"name"`
279 Icon template.HTML `json:"icon"`
280 ShortDescription string `json:"shortDescription"`
281 Slug string `json:"slug"`
282 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
283}
284
gio59946282024-10-07 12:55:51 +0400285func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400286 all, err := s.r.GetAll()
287 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400288 http.Error(w, err.Error(), http.StatusInternalServerError)
289 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400290 }
291 resp := make([]app, len(all))
292 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400293 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400294 }
gioaa0fcdb2024-06-10 22:19:25 +0400295 w.Header().Set("Content-Type", "application/json")
296 if err := json.NewEncoder(w).Encode(resp); err != nil {
297 http.Error(w, err.Error(), http.StatusInternalServerError)
298 return
299 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400300}
301
gio59946282024-10-07 12:55:51 +0400302func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400303 slug, ok := mux.Vars(r)["slug"]
304 if !ok {
305 http.Error(w, "empty slug", http.StatusBadRequest)
306 return
307 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400308 a, err := s.r.Find(slug)
309 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400310 http.Error(w, err.Error(), http.StatusInternalServerError)
311 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400312 }
gio7fbd4ad2024-08-27 10:06:39 +0400313 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400314 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400315 http.Error(w, err.Error(), http.StatusInternalServerError)
316 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400317 }
gioaa0fcdb2024-06-10 22:19:25 +0400318 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
319 w.Header().Set("Content-Type", "application/json")
320 if err := json.NewEncoder(w).Encode(resp); err != nil {
321 http.Error(w, err.Error(), http.StatusInternalServerError)
322 return
323 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400324}
325
gio59946282024-10-07 12:55:51 +0400326func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400327 slug, ok := mux.Vars(r)["slug"]
328 if !ok {
329 http.Error(w, "empty slug", http.StatusBadRequest)
330 return
331 }
gio7fbd4ad2024-08-27 10:06:39 +0400332 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400333 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400334 http.Error(w, err.Error(), http.StatusInternalServerError)
335 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400336 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400337 a, err := s.r.Find(instance.AppId)
338 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400339 http.Error(w, err.Error(), http.StatusInternalServerError)
340 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400341 }
gioaa0fcdb2024-06-10 22:19:25 +0400342 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
343 w.Header().Set("Content-Type", "application/json")
344 if err := json.NewEncoder(w).Encode(resp); err != nil {
345 http.Error(w, err.Error(), http.StatusInternalServerError)
346 return
347 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400348}
349
gio218e8132025-04-22 17:11:58 +0000350func (s *Server) install(app installer.EnvApp, values map[string]any) (string, installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400351 env, err := s.m.Config()
352 if err != nil {
gio218e8132025-04-22 17:11:58 +0000353 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400354 }
355 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
356 suffix, err := suffixGen.Generate()
357 if err != nil {
gio218e8132025-04-22 17:11:58 +0000358 return "", installer.ReleaseResources{}, err
gioa421b062025-04-21 09:45:04 +0400359 }
360 instanceId := app.Slug() + suffix
361 appDir := fmt.Sprintf("/apps/%s", instanceId)
362 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
gio218e8132025-04-22 17:11:58 +0000363 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
gioa421b062025-04-21 09:45:04 +0400364 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gioa421b062025-04-21 09:45:04 +0400365 if err == nil {
366 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
367 go s.reconciler.Reconcile(ctx)
368 }
369 return rr, err
370 })
371 if _, ok := s.tasks[instanceId]; ok {
372 panic("MUST NOT REACH!")
373 }
374 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
375 t.OnDone(s.cleanTask(instanceId, 0))
376 go t.Start()
gio218e8132025-04-22 17:11:58 +0000377 return instanceId, rr, nil
gioa421b062025-04-21 09:45:04 +0400378}
379
gio59946282024-10-07 12:55:51 +0400380func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400381 s.l.Lock()
382 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400383 slug, ok := mux.Vars(r)["slug"]
384 if !ok {
385 http.Error(w, "empty slug", http.StatusBadRequest)
386 return
387 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400388 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400389 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400390 http.Error(w, err.Error(), http.StatusInternalServerError)
391 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400392 }
gioa421b062025-04-21 09:45:04 +0400393 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400394 if 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 }
gio218e8132025-04-22 17:11:58 +0000398 if instanceId, _, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400399 http.Error(w, err.Error(), http.StatusInternalServerError)
400 return
gioa421b062025-04-21 09:45:04 +0400401 } else {
402 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400403 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400404}
405
gio59946282024-10-07 12:55:51 +0400406func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400407 s.l.Lock()
408 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400409 slug, ok := mux.Vars(r)["slug"]
410 if !ok {
411 http.Error(w, "empty slug", http.StatusBadRequest)
412 return
413 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400414 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400415 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400416 http.Error(w, err.Error(), http.StatusInternalServerError)
417 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400418 }
gio8c876172024-10-05 12:25:13 +0400419 tid := 0
420 if t, ok := s.tasks[slug]; ok {
421 if t.task != nil {
422 http.Error(w, "Update already in progress", http.StatusBadRequest)
423 return
424 }
425 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400426 }
giof8843412024-05-22 16:38:05 +0400427 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400428 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400429 http.Error(w, err.Error(), http.StatusInternalServerError)
430 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400431 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400432 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
433 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400434 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400435 t.OnDone(s.cleanTask(slug, tid))
436 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400437 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400438 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400439 http.Error(w, err.Error(), http.StatusInternalServerError)
440 return
441 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400442}
443
gio59946282024-10-07 12:55:51 +0400444func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400445 slug, ok := mux.Vars(r)["slug"]
446 if !ok {
447 http.Error(w, "empty slug", http.StatusBadRequest)
448 return
449 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400450 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400451 http.Error(w, err.Error(), http.StatusInternalServerError)
452 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400453 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400454 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
455 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400456 if _, err := fmt.Fprint(w, "/"); err != nil {
457 http.Error(w, err.Error(), http.StatusInternalServerError)
458 return
459 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400460}
461
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400462type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400463 Apps []app
464 CurrentPage string
465 SearchTarget string
466 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400467}
468
gio59946282024-10-07 12:55:51 +0400469func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400470 pageType := mux.Vars(r)["pageType"]
471 if pageType == "" {
472 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400473 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400474 searchQuery := r.FormValue("query")
475 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400476 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400477 http.Error(w, err.Error(), http.StatusInternalServerError)
478 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400479 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400480 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400481 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400482 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400483 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400484 http.Error(w, err.Error(), http.StatusInternalServerError)
485 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400486 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400487 switch pageType {
488 case "installed":
489 if len(instances) != 0 {
490 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
491 }
492 case "not-installed":
493 if len(instances) == 0 {
494 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
495 }
496 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400497 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
498 }
499 }
500 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400501 Apps: resp,
502 CurrentPage: pageType,
503 SearchTarget: pageType,
504 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400505 }
gioaa0fcdb2024-06-10 22:19:25 +0400506 if err := s.tmpl.index.Execute(w, data); err != nil {
507 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400508 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400509}
510
511type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400512 App installer.EnvApp
513 Instance *installer.AppInstanceConfig
514 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400515 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400516 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400517 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400518}
519
gio59946282024-10-07 12:55:51 +0400520func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400521 global, err := s.m.Config()
522 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400523 http.Error(w, err.Error(), http.StatusInternalServerError)
524 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400525 }
gioaa0fcdb2024-06-10 22:19:25 +0400526 slug, ok := mux.Vars(r)["slug"]
527 if !ok {
528 http.Error(w, "empty slug", http.StatusBadRequest)
529 return
530 }
gio3cdee592024-04-17 10:15:56 +0400531 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400532 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400533 http.Error(w, err.Error(), http.StatusInternalServerError)
534 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400535 }
gio7fbd4ad2024-08-27 10:06:39 +0400536 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400537 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400538 http.Error(w, err.Error(), http.StatusInternalServerError)
539 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400540 }
giocb34ad22024-07-11 08:01:13 +0400541 networks, err := s.m.CreateNetworks(global)
542 if err != nil {
543 http.Error(w, err.Error(), http.StatusInternalServerError)
544 return
545 }
giof6ad2982024-08-23 17:42:49 +0400546 clusters, err := s.m.GetClusters()
547 if err != nil {
548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
550 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400551 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552 App: a,
553 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400554 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400555 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400556 CurrentPage: a.Name(),
557 }
gioaa0fcdb2024-06-10 22:19:25 +0400558 if err := s.tmpl.app.Execute(w, data); err != nil {
559 http.Error(w, err.Error(), http.StatusInternalServerError)
560 return
561 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400562}
563
gio59946282024-10-07 12:55:51 +0400564func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400565 s.l.Lock()
566 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400567 global, err := s.m.Config()
568 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400569 http.Error(w, err.Error(), http.StatusInternalServerError)
570 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 }
gioaa0fcdb2024-06-10 22:19:25 +0400572 slug, ok := mux.Vars(r)["slug"]
573 if !ok {
574 http.Error(w, "empty slug", http.StatusBadRequest)
575 return
576 }
gio8c876172024-10-05 12:25:13 +0400577 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400578 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
579 return
580 }
gio8c876172024-10-05 12:25:13 +0400581 instance, err := s.m.GetInstance(slug)
582 if err != nil {
583 http.Error(w, err.Error(), http.StatusInternalServerError)
584 return
585 }
586 a, err := s.m.GetInstanceApp(instance.Id)
587 if err != nil {
588 http.Error(w, err.Error(), http.StatusInternalServerError)
589 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400590 }
gio7fbd4ad2024-08-27 10:06:39 +0400591 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400592 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400593 http.Error(w, err.Error(), http.StatusInternalServerError)
594 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400595 }
giocb34ad22024-07-11 08:01:13 +0400596 networks, err := s.m.CreateNetworks(global)
597 if err != nil {
598 http.Error(w, err.Error(), http.StatusInternalServerError)
599 return
600 }
giof6ad2982024-08-23 17:42:49 +0400601 clusters, err := s.m.GetClusters()
602 if err != nil {
603 http.Error(w, err.Error(), http.StatusInternalServerError)
604 return
605 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400606 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400607 App: a,
gio778577f2024-04-29 09:44:38 +0400608 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400609 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400610 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400611 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400612 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400613 }
gioaa0fcdb2024-06-10 22:19:25 +0400614 if err := s.tmpl.app.Execute(w, data); err != nil {
615 http.Error(w, err.Error(), http.StatusInternalServerError)
616 return
617 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400618}
giof6ad2982024-08-23 17:42:49 +0400619
620type taskStatusData struct {
621 CurrentPage string
622 Task tasks.Task
623}
624
gio59946282024-10-07 12:55:51 +0400625func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400626 s.l.Lock()
627 defer s.l.Unlock()
628 slug, ok := mux.Vars(r)["slug"]
629 if !ok {
630 http.Error(w, "empty slug", http.StatusBadRequest)
631 return
632 }
633 t, ok := s.tasks[slug]
634 if !ok {
635 http.Error(w, "task not found", http.StatusInternalServerError)
636
637 return
638 }
gio8c876172024-10-05 12:25:13 +0400639 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400640 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
641 return
642 }
643 data := taskStatusData{
644 CurrentPage: "",
645 Task: t.task,
646 }
647 if err := s.tmpl.task.Execute(w, data); err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
651}
652
653type clustersData struct {
654 CurrentPage string
655 Clusters []cluster.State
656}
657
gio59946282024-10-07 12:55:51 +0400658func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400659 clusters, err := s.m.GetClusters()
660 if err != nil {
661 http.Error(w, err.Error(), http.StatusInternalServerError)
662 return
663 }
664 data := clustersData{
665 "clusters",
666 clusters,
667 }
668 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
669 http.Error(w, err.Error(), http.StatusInternalServerError)
670 return
671 }
672}
673
674type clusterData struct {
675 CurrentPage string
676 Cluster cluster.State
677}
678
gio59946282024-10-07 12:55:51 +0400679func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400680 name, ok := mux.Vars(r)["name"]
681 if !ok {
682 http.Error(w, "empty name", http.StatusBadRequest)
683 return
684 }
685 m, err := s.getClusterManager(name)
686 if err != nil {
687 if errors.Is(err, installer.ErrorNotFound) {
688 http.Error(w, "not found", http.StatusNotFound)
689 } else {
690 http.Error(w, err.Error(), http.StatusInternalServerError)
691 }
692 return
693 }
694 data := clusterData{
695 "clusters",
696 m.State(),
697 }
698 if err := s.tmpl.cluster.Execute(w, data); err != nil {
699 http.Error(w, err.Error(), http.StatusInternalServerError)
700 return
701 }
702}
703
gio59946282024-10-07 12:55:51 +0400704func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400705 cName, ok := mux.Vars(r)["name"]
706 if !ok {
707 http.Error(w, "empty name", http.StatusBadRequest)
708 return
709 }
gio8c876172024-10-05 12:25:13 +0400710 tid := 0
711 if t, ok := s.tasks[cName]; ok {
712 if t.task != nil {
713 http.Error(w, "cluster task in progress", http.StatusLocked)
714 return
715 }
716 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400717 }
718 m, err := s.getClusterManager(cName)
719 if err != nil {
720 if errors.Is(err, installer.ErrorNotFound) {
721 http.Error(w, "not found", http.StatusNotFound)
722 } else {
723 http.Error(w, err.Error(), http.StatusInternalServerError)
724 }
725 return
726 }
727 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400728 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400729 go task.Start()
gio8c876172024-10-05 12:25:13 +0400730 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400731 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
732}
733
gio59946282024-10-07 12:55:51 +0400734func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400735 s.l.Lock()
736 defer s.l.Unlock()
737 cName, ok := mux.Vars(r)["cluster"]
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
giof6ad2982024-08-23 17:42:49 +0400749 }
750 sName, ok := mux.Vars(r)["server"]
751 if !ok {
752 http.Error(w, "empty name", http.StatusBadRequest)
753 return
754 }
755 m, err := s.getClusterManager(cName)
756 if err != nil {
757 if errors.Is(err, installer.ErrorNotFound) {
758 http.Error(w, "not found", http.StatusNotFound)
759 } else {
760 http.Error(w, err.Error(), http.StatusInternalServerError)
761 }
762 return
763 }
764 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400765 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400766 go task.Start()
gio8c876172024-10-05 12:25:13 +0400767 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400768 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
769}
770
gio59946282024-10-07 12:55:51 +0400771func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400772 clusters, err := s.m.GetClusters()
773 if err != nil {
774 return nil, err
775 }
776 var c *cluster.State
777 for _, i := range clusters {
778 if i.Name == cName {
779 c = &i
780 break
781 }
782 }
783 if c == nil {
784 return nil, installer.ErrorNotFound
785 }
786 return cluster.RestoreKubeManager(*c)
787}
788
gio59946282024-10-07 12:55:51 +0400789func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400790 s.l.Lock()
791 defer s.l.Unlock()
792 cName, ok := mux.Vars(r)["cluster"]
793 if !ok {
794 http.Error(w, "empty name", http.StatusBadRequest)
795 return
796 }
gio8c876172024-10-05 12:25:13 +0400797 tid := 0
798 if t, ok := s.tasks[cName]; ok {
799 if t.task != nil {
800 http.Error(w, "cluster task in progress", http.StatusLocked)
801 return
802 }
803 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400804 }
805 m, err := s.getClusterManager(cName)
806 if err != nil {
807 if errors.Is(err, installer.ErrorNotFound) {
808 http.Error(w, "not found", http.StatusNotFound)
809 } else {
810 http.Error(w, err.Error(), http.StatusInternalServerError)
811 }
812 return
813 }
814 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400815 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400816 if ip == nil {
817 http.Error(w, "invalid ip", http.StatusBadRequest)
818 return
819 }
820 port := 22
821 if p := r.PostFormValue("port"); p != "" {
822 port, err = strconv.Atoi(p)
823 if err != nil {
824 http.Error(w, err.Error(), http.StatusBadRequest)
825 return
826 }
827 }
828 server := cluster.Server{
829 IP: ip,
830 Port: port,
831 User: r.PostFormValue("user"),
832 Password: r.PostFormValue("password"),
833 }
834 var task tasks.Task
835 switch strings.ToLower(t) {
836 case "controller":
837 if len(m.State().Controllers) == 0 {
838 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
839 } else {
840 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
841 }
842 case "worker":
843 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
844 default:
845 http.Error(w, "invalid type", http.StatusBadRequest)
846 return
847 }
gio8c876172024-10-05 12:25:13 +0400848 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400849 go task.Start()
gio8c876172024-10-05 12:25:13 +0400850 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400851 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
852}
853
gio59946282024-10-07 12:55:51 +0400854func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400855 cName := r.PostFormValue("name")
856 if cName == "" {
857 http.Error(w, "no name", http.StatusBadRequest)
858 return
859 }
860 st := cluster.State{Name: cName}
861 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
862 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
863 return "", err
864 }
865 return fmt.Sprintf("create cluster: %s", cName), nil
866 }); err != nil {
867 http.Error(w, err.Error(), http.StatusInternalServerError)
868 return
869 }
870 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
871}
872
gio59946282024-10-07 12:55:51 +0400873func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400874 cName, ok := mux.Vars(r)["name"]
875 if !ok {
876 http.Error(w, "empty name", http.StatusBadRequest)
877 return
878 }
gio8c876172024-10-05 12:25:13 +0400879 tid := 0
880 if t, ok := s.tasks[cName]; ok {
881 if t.task != nil {
882 http.Error(w, "cluster task in progress", http.StatusLocked)
883 return
884 }
885 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400886 }
887 m, err := s.getClusterManager(cName)
888 if err != nil {
889 if errors.Is(err, installer.ErrorNotFound) {
890 http.Error(w, "not found", http.StatusNotFound)
891 } else {
892 http.Error(w, err.Error(), http.StatusInternalServerError)
893 }
894 return
895 }
896 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400897 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400898 go task.Start()
gio8c876172024-10-05 12:25:13 +0400899 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400900 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
901}
902
gio59946282024-10-07 12:55:51 +0400903func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400904 const vpnUser = "private-network-proxy"
905 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
906 hostname := fmt.Sprintf("cluster-%s", name)
907 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
908 app, err := installer.FindEnvApp(s.fr, "cluster-network")
909 if err != nil {
910 return installer.ReleaseResources{}, err
911 }
912 env, err := s.m.Config()
913 if err != nil {
914 return installer.ReleaseResources{}, err
915 }
gio721c0042025-04-03 11:56:36 +0400916 keys, err := installer.NewSSHKeyPair("port-allocator")
917 if err != nil {
918 return installer.ReleaseResources{}, err
919 }
920 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
921 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
922 return installer.ReleaseResources{}, err
923 }
924 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
925 return installer.ReleaseResources{}, err
926 }
giof6ad2982024-08-23 17:42:49 +0400927 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
928 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400929 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400930 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
931 "cluster": map[string]any{
932 "name": name,
933 "kubeconfig": kubeconfig,
934 "ingressClassName": ingressClassName,
935 },
936 // TODO(gio): remove hardcoded user
937 "vpnUser": vpnUser,
938 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +0400939 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +0400940 })
941 if err != nil {
942 return installer.ReleaseResources{}, err
943 }
944 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
945 go s.reconciler.Reconcile(ctx)
946 return rr, err
947 })
948 ch := make(chan error)
949 t.OnDone(func(err error) {
950 ch <- err
951 })
952 go t.Start()
953 err := <-ch
954 if err != nil {
955 return nil, err
956 }
957 for {
958 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
959 if err == nil {
960 return ip, nil
961 }
962 if errors.Is(err, installer.ErrorNotFound) {
963 time.Sleep(5 * time.Second)
964 }
965 }
966 }
967}
gio8f290322024-09-21 15:37:45 +0400968
gio59946282024-10-07 12:55:51 +0400969func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +0400970 return func(cm cluster.Manager) error {
971 name := cm.State().Name
972 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
973 app, err := installer.FindEnvApp(s.fr, "longhorn")
974 if err != nil {
975 return installer.ReleaseResources{}, err
976 }
977 env, err := s.m.Config()
978 if err != nil {
979 return installer.ReleaseResources{}, err
980 }
981 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
982 appDir := fmt.Sprintf("/clusters/%s/storage", name)
983 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
984 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
985 "cluster": name,
986 })
987 if err != nil {
988 return installer.ReleaseResources{}, err
989 }
990 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
991 go s.reconciler.Reconcile(ctx)
992 return rr, err
993 })
994 ch := make(chan error)
995 t.OnDone(func(err error) {
996 ch <- err
997 })
998 go t.Start()
999 err := <-ch
1000 if err != nil {
1001 return err
1002 }
1003 cm.EnableStorage()
1004 return nil
1005 }
1006}
gio8c876172024-10-05 12:25:13 +04001007
gio59946282024-10-07 12:55:51 +04001008func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +04001009 return func(err error) {
1010 if err != nil {
1011 fmt.Printf("Task %s failed: %s", name, err.Error())
1012 }
1013 s.l.Lock()
1014 defer s.l.Unlock()
1015 s.tasks[name].task = nil
1016 go func() {
1017 time.Sleep(30 * time.Second)
1018 s.l.Lock()
1019 defer s.l.Unlock()
1020 if t, ok := s.tasks[name]; ok && t.id == id {
1021 delete(s.tasks, name)
1022 }
1023 }()
1024 }
1025}