blob: 5e1e06beb79301625c732db072858c6e38cd7078 [file] [log] [blame]
gio59946282024-10-07 12:55:51 +04001package appmanager
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04002
3import (
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +04004 "context"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04005 "embed"
6 "encoding/json"
giof6ad2982024-08-23 17:42:49 +04007 "errors"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04008 "fmt"
9 "html/template"
giof6ad2982024-08-23 17:42:49 +040010 "net"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040011 "net/http"
giof6ad2982024-08-23 17:42:49 +040012 "strconv"
13 "strings"
14 "sync"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040015 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040016
17 "github.com/Masterminds/sprig/v3"
gioaa0fcdb2024-06-10 22:19:25 +040018 "github.com/gorilla/mux"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040019
20 "github.com/giolekva/pcloud/core/installer"
giof6ad2982024-08-23 17:42:49 +040021 "github.com/giolekva/pcloud/core/installer/cluster"
gio59946282024-10-07 12:55:51 +040022 "github.com/giolekva/pcloud/core/installer/server"
giof6ad2982024-08-23 17:42:49 +040023 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040024 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040025)
26
gio59946282024-10-07 12:55:51 +040027//go:embed templates/*
28var templates embed.FS
29
30//go:embed static/*
31var staticAssets embed.FS
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040032
giof6ad2982024-08-23 17:42:49 +040033type taskForward struct {
34 task tasks.Task
35 redirectTo string
gio8c876172024-10-05 12:25:13 +040036 id int
giof6ad2982024-08-23 17:42:49 +040037}
38
gio59946282024-10-07 12:55:51 +040039type Server struct {
giof6ad2982024-08-23 17:42:49 +040040 l sync.Locker
41 port int
42 repo soft.RepoIO
43 m *installer.AppManager
44 r installer.AppRepository
45 fr installer.AppRepository
46 reconciler *tasks.FixedReconciler
47 h installer.HelmReleaseMonitor
48 cnc installer.ClusterNetworkConfigurator
49 vpnAPIClient installer.VPNAPIClient
gio8c876172024-10-05 12:25:13 +040050 tasks map[string]*taskForward
giof6ad2982024-08-23 17:42:49 +040051 tmpl tmplts
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040052}
53
54type tmplts struct {
giof6ad2982024-08-23 17:42:49 +040055 index *template.Template
56 app *template.Template
57 allClusters *template.Template
58 cluster *template.Template
59 task *template.Template
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040060}
61
gio59946282024-10-07 12:55:51 +040062func parseTemplates(fs embed.FS) (tmplts, error) {
63 base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "templates/base.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040064 if err != nil {
65 return tmplts{}, err
66 }
67 parse := func(path string) (*template.Template, error) {
68 if b, err := base.Clone(); err != nil {
69 return nil, err
70 } else {
71 return b.ParseFS(fs, path)
72 }
73 }
gio59946282024-10-07 12:55:51 +040074 index, err := parse("templates/index.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040075 if err != nil {
76 return tmplts{}, err
77 }
gio59946282024-10-07 12:55:51 +040078 app, err := parse("templates/app.html")
Davit Tabidze3ec24cf2024-05-22 14:06:02 +040079 if err != nil {
80 return tmplts{}, err
81 }
gio59946282024-10-07 12:55:51 +040082 allClusters, err := parse("templates/all-clusters.html")
giof6ad2982024-08-23 17:42:49 +040083 if err != nil {
84 return tmplts{}, err
85 }
gio59946282024-10-07 12:55:51 +040086 cluster, err := parse("templates/cluster.html")
giof6ad2982024-08-23 17:42:49 +040087 if err != nil {
88 return tmplts{}, err
89 }
gio59946282024-10-07 12:55:51 +040090 task, err := parse("templates/task.html")
giof6ad2982024-08-23 17:42:49 +040091 if err != nil {
92 return tmplts{}, err
93 }
94 return tmplts{index, app, allClusters, cluster, task}, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040095}
96
gio59946282024-10-07 12:55:51 +040097func NewServer(
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040098 port int,
giof6ad2982024-08-23 17:42:49 +040099 repo soft.RepoIO,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400100 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400101 r installer.AppRepository,
giof6ad2982024-08-23 17:42:49 +0400102 fr installer.AppRepository,
gio43b0f422024-08-21 10:40:13 +0400103 reconciler *tasks.FixedReconciler,
gio778577f2024-04-29 09:44:38 +0400104 h installer.HelmReleaseMonitor,
giof6ad2982024-08-23 17:42:49 +0400105 cnc installer.ClusterNetworkConfigurator,
106 vpnAPIClient installer.VPNAPIClient,
gio59946282024-10-07 12:55:51 +0400107) (*Server, error) {
108 tmpl, err := parseTemplates(templates)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400109 if err != nil {
110 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400111 }
gio59946282024-10-07 12:55:51 +0400112 return &Server{
giof6ad2982024-08-23 17:42:49 +0400113 l: &sync.Mutex{},
114 port: port,
115 repo: repo,
116 m: m,
117 r: r,
118 fr: fr,
119 reconciler: reconciler,
120 h: h,
121 cnc: cnc,
122 vpnAPIClient: vpnAPIClient,
gio8c876172024-10-05 12:25:13 +0400123 tasks: make(map[string]*taskForward),
giof6ad2982024-08-23 17:42:49 +0400124 tmpl: tmpl,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400125 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400126}
127
gio59946282024-10-07 12:55:51 +0400128func (s *Server) Start() error {
gioaa0fcdb2024-06-10 22:19:25 +0400129 r := mux.NewRouter()
gio59946282024-10-07 12:55:51 +0400130 r.PathPrefix("/static/").Handler(server.NewCachingHandler(http.FileServer(http.FS(staticAssets))))
giocb34ad22024-07-11 08:01:13 +0400131 r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
giof15b9da2024-09-19 06:59:16 +0400132 r.HandleFunc("/api/clusters", s.handleClusters).Methods(http.MethodGet)
133 r.HandleFunc("/api/proxy/add", s.handleProxyAdd).Methods(http.MethodPost)
134 r.HandleFunc("/api/proxy/remove", s.handleProxyRemove).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400135 r.HandleFunc("/api/app-repo", s.handleAppRepo)
136 r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
137 r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
138 r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
139 r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
140 r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400141 r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
142 r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
143 r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
gio8f290322024-09-21 15:37:45 +0400144 r.HandleFunc("/clusters/{name}/setup-storage", s.handleClusterSetupStorage).Methods(http.MethodPost)
giof6ad2982024-08-23 17:42:49 +0400145 r.HandleFunc("/clusters/{name}/remove", s.handleRemoveCluster).Methods(http.MethodPost)
146 r.HandleFunc("/clusters", s.handleAllClusters).Methods(http.MethodGet)
147 r.HandleFunc("/clusters", s.handleCreateCluster).Methods(http.MethodPost)
gioaa0fcdb2024-06-10 22:19:25 +0400148 r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
149 r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
giof6ad2982024-08-23 17:42:49 +0400150 r.HandleFunc("/tasks/{slug}", s.handleTaskStatus).Methods(http.MethodGet)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400151 r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
152 r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400153 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
gioaa0fcdb2024-06-10 22:19:25 +0400154 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400155}
156
gio59946282024-10-07 12:55:51 +0400157func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400158 env, err := s.m.Config()
159 if err != nil {
160 http.Error(w, err.Error(), http.StatusInternalServerError)
161 return
162 }
163 networks, err := s.m.CreateNetworks(env)
164 if err != nil {
165 http.Error(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
168 if err := json.NewEncoder(w).Encode(networks); err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
170 return
171 }
172}
173
gio59946282024-10-07 12:55:51 +0400174func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400175 clusters, err := s.m.GetClusters()
176 if err != nil {
177 http.Error(w, err.Error(), http.StatusInternalServerError)
178 return
179 }
180 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
181 http.Error(w, err.Error(), http.StatusInternalServerError)
182 return
183 }
184}
185
186type proxyPair struct {
187 From string `json:"from"`
188 To string `json:"to"`
189}
190
gio59946282024-10-07 12:55:51 +0400191func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400192 var req proxyPair
193 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
194 http.Error(w, err.Error(), http.StatusBadRequest)
195 return
196 }
197 if err := s.cnc.AddProxy(req.From, req.To); err != nil {
198 http.Error(w, err.Error(), http.StatusInternalServerError)
199 return
200 }
201}
202
gio59946282024-10-07 12:55:51 +0400203func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400204 var req proxyPair
205 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
206 http.Error(w, err.Error(), http.StatusBadRequest)
207 return
208 }
209 if err := s.cnc.RemoveProxy(req.From, req.To); err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 }
213}
214
215type app struct {
216 Name string `json:"name"`
217 Icon template.HTML `json:"icon"`
218 ShortDescription string `json:"shortDescription"`
219 Slug string `json:"slug"`
220 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
221}
222
gio59946282024-10-07 12:55:51 +0400223func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400224 all, err := s.r.GetAll()
225 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400226 http.Error(w, err.Error(), http.StatusInternalServerError)
227 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400228 }
229 resp := make([]app, len(all))
230 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400231 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400232 }
gioaa0fcdb2024-06-10 22:19:25 +0400233 w.Header().Set("Content-Type", "application/json")
234 if err := json.NewEncoder(w).Encode(resp); err != nil {
235 http.Error(w, err.Error(), http.StatusInternalServerError)
236 return
237 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400238}
239
gio59946282024-10-07 12:55:51 +0400240func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400241 slug, ok := mux.Vars(r)["slug"]
242 if !ok {
243 http.Error(w, "empty slug", http.StatusBadRequest)
244 return
245 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400246 a, err := s.r.Find(slug)
247 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400248 http.Error(w, err.Error(), http.StatusInternalServerError)
249 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400250 }
gio7fbd4ad2024-08-27 10:06:39 +0400251 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400252 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400253 http.Error(w, err.Error(), http.StatusInternalServerError)
254 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400255 }
gioaa0fcdb2024-06-10 22:19:25 +0400256 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
257 w.Header().Set("Content-Type", "application/json")
258 if err := json.NewEncoder(w).Encode(resp); err != nil {
259 http.Error(w, err.Error(), http.StatusInternalServerError)
260 return
261 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400262}
263
gio59946282024-10-07 12:55:51 +0400264func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400265 slug, ok := mux.Vars(r)["slug"]
266 if !ok {
267 http.Error(w, "empty slug", http.StatusBadRequest)
268 return
269 }
gio7fbd4ad2024-08-27 10:06:39 +0400270 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400271 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400274 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400275 a, err := s.r.Find(instance.AppId)
276 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400277 http.Error(w, err.Error(), http.StatusInternalServerError)
278 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400279 }
gioaa0fcdb2024-06-10 22:19:25 +0400280 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
281 w.Header().Set("Content-Type", "application/json")
282 if err := json.NewEncoder(w).Encode(resp); err != nil {
283 http.Error(w, err.Error(), http.StatusInternalServerError)
284 return
285 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400286}
287
gio59946282024-10-07 12:55:51 +0400288func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400289 s.l.Lock()
290 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400291 slug, ok := mux.Vars(r)["slug"]
292 if !ok {
293 http.Error(w, "empty slug", http.StatusBadRequest)
294 return
295 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400296 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400297 if err := json.NewDecoder(r.Body).Decode(&values); 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 }
gio3cdee592024-04-17 10:15:56 +0400301 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400302 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400303 http.Error(w, err.Error(), http.StatusInternalServerError)
304 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400305 }
gio3cdee592024-04-17 10:15:56 +0400306 env, err := s.m.Config()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400307 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400308 http.Error(w, err.Error(), http.StatusInternalServerError)
309 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400310 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400311 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400312 suffix, err := suffixGen.Generate()
313 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400314 http.Error(w, err.Error(), http.StatusInternalServerError)
315 return
gio3af43942024-04-16 08:13:50 +0400316 }
gio44f621b2024-04-29 09:44:38 +0400317 instanceId := a.Slug() + suffix
gio3cdee592024-04-17 10:15:56 +0400318 appDir := fmt.Sprintf("/apps/%s", instanceId)
319 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
gio1cd65152024-08-16 08:18:49 +0400320 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
gio43b0f422024-08-21 10:40:13 +0400321 rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
322 if err == nil {
323 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
324 go s.reconciler.Reconcile(ctx)
325 }
326 return rr, err
gio1cd65152024-08-16 08:18:49 +0400327 })
gio778577f2024-04-29 09:44:38 +0400328 if _, ok := s.tasks[instanceId]; ok {
329 panic("MUST NOT REACH!")
330 }
gio8c876172024-10-05 12:25:13 +0400331 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
332 t.OnDone(s.cleanTask(instanceId, 0))
gio778577f2024-04-29 09:44:38 +0400333 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400334 if _, err := fmt.Fprintf(w, "/tasks/%s", instanceId); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400335 http.Error(w, err.Error(), http.StatusInternalServerError)
336 return
337 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400338}
339
gio59946282024-10-07 12:55:51 +0400340func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400341 s.l.Lock()
342 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400343 slug, ok := mux.Vars(r)["slug"]
344 if !ok {
345 http.Error(w, "empty slug", http.StatusBadRequest)
346 return
347 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400348 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400349 if err := json.NewDecoder(r.Body).Decode(&values); 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 }
gio8c876172024-10-05 12:25:13 +0400353 tid := 0
354 if t, ok := s.tasks[slug]; ok {
355 if t.task != nil {
356 http.Error(w, "Update already in progress", http.StatusBadRequest)
357 return
358 }
359 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400360 }
giof8843412024-05-22 16:38:05 +0400361 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400362 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400363 http.Error(w, err.Error(), http.StatusInternalServerError)
364 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400365 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400366 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
367 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400368 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400369 t.OnDone(s.cleanTask(slug, tid))
370 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400371 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400372 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 return
375 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400376}
377
gio59946282024-10-07 12:55:51 +0400378func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400379 slug, ok := mux.Vars(r)["slug"]
380 if !ok {
381 http.Error(w, "empty slug", http.StatusBadRequest)
382 return
383 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400384 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400385 http.Error(w, err.Error(), http.StatusInternalServerError)
386 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400387 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400388 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
389 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400390 if _, err := fmt.Fprint(w, "/"); err != nil {
391 http.Error(w, err.Error(), http.StatusInternalServerError)
392 return
393 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400394}
395
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400396type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400397 Apps []app
398 CurrentPage string
399 SearchTarget string
400 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400401}
402
gio59946282024-10-07 12:55:51 +0400403func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400404 pageType := mux.Vars(r)["pageType"]
405 if pageType == "" {
406 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400407 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400408 searchQuery := r.FormValue("query")
409 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400410 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400413 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400414 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400415 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400416 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400417 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400420 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400421 switch pageType {
422 case "installed":
423 if len(instances) != 0 {
424 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
425 }
426 case "not-installed":
427 if len(instances) == 0 {
428 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
429 }
430 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400431 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
432 }
433 }
434 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400435 Apps: resp,
436 CurrentPage: pageType,
437 SearchTarget: pageType,
438 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400439 }
gioaa0fcdb2024-06-10 22:19:25 +0400440 if err := s.tmpl.index.Execute(w, data); err != nil {
441 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400442 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400443}
444
445type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400446 App installer.EnvApp
447 Instance *installer.AppInstanceConfig
448 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400449 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400450 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400451 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400452}
453
gio59946282024-10-07 12:55:51 +0400454func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400455 global, err := s.m.Config()
456 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400457 http.Error(w, err.Error(), http.StatusInternalServerError)
458 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400459 }
gioaa0fcdb2024-06-10 22:19:25 +0400460 slug, ok := mux.Vars(r)["slug"]
461 if !ok {
462 http.Error(w, "empty slug", http.StatusBadRequest)
463 return
464 }
gio3cdee592024-04-17 10:15:56 +0400465 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400466 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400467 http.Error(w, err.Error(), http.StatusInternalServerError)
468 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400469 }
gio7fbd4ad2024-08-27 10:06:39 +0400470 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400471 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400472 http.Error(w, err.Error(), http.StatusInternalServerError)
473 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400474 }
giocb34ad22024-07-11 08:01:13 +0400475 networks, err := s.m.CreateNetworks(global)
476 if err != nil {
477 http.Error(w, err.Error(), http.StatusInternalServerError)
478 return
479 }
giof6ad2982024-08-23 17:42:49 +0400480 clusters, err := s.m.GetClusters()
481 if err != nil {
482 http.Error(w, err.Error(), http.StatusInternalServerError)
483 return
484 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400485 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400486 App: a,
487 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400488 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400489 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400490 CurrentPage: a.Name(),
491 }
gioaa0fcdb2024-06-10 22:19:25 +0400492 if err := s.tmpl.app.Execute(w, data); err != nil {
493 http.Error(w, err.Error(), http.StatusInternalServerError)
494 return
495 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400496}
497
gio59946282024-10-07 12:55:51 +0400498func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400499 s.l.Lock()
500 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400501 global, err := s.m.Config()
502 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400503 http.Error(w, err.Error(), http.StatusInternalServerError)
504 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400505 }
gioaa0fcdb2024-06-10 22:19:25 +0400506 slug, ok := mux.Vars(r)["slug"]
507 if !ok {
508 http.Error(w, "empty slug", http.StatusBadRequest)
509 return
510 }
gio8c876172024-10-05 12:25:13 +0400511 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400512 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
513 return
514 }
gio8c876172024-10-05 12:25:13 +0400515 instance, err := s.m.GetInstance(slug)
516 if err != nil {
517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
519 }
520 a, err := s.m.GetInstanceApp(instance.Id)
521 if err != nil {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400524 }
gio7fbd4ad2024-08-27 10:06:39 +0400525 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400526 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400529 }
giocb34ad22024-07-11 08:01:13 +0400530 networks, err := s.m.CreateNetworks(global)
531 if err != nil {
532 http.Error(w, err.Error(), http.StatusInternalServerError)
533 return
534 }
giof6ad2982024-08-23 17:42:49 +0400535 clusters, err := s.m.GetClusters()
536 if err != nil {
537 http.Error(w, err.Error(), http.StatusInternalServerError)
538 return
539 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400540 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400541 App: a,
gio778577f2024-04-29 09:44:38 +0400542 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400543 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400544 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400545 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400546 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400547 }
gioaa0fcdb2024-06-10 22:19:25 +0400548 if err := s.tmpl.app.Execute(w, data); err != nil {
549 http.Error(w, err.Error(), http.StatusInternalServerError)
550 return
551 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552}
giof6ad2982024-08-23 17:42:49 +0400553
554type taskStatusData struct {
555 CurrentPage string
556 Task tasks.Task
557}
558
gio59946282024-10-07 12:55:51 +0400559func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400560 s.l.Lock()
561 defer s.l.Unlock()
562 slug, ok := mux.Vars(r)["slug"]
563 if !ok {
564 http.Error(w, "empty slug", http.StatusBadRequest)
565 return
566 }
567 t, ok := s.tasks[slug]
568 if !ok {
569 http.Error(w, "task not found", http.StatusInternalServerError)
570
571 return
572 }
gio8c876172024-10-05 12:25:13 +0400573 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400574 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
575 return
576 }
577 data := taskStatusData{
578 CurrentPage: "",
579 Task: t.task,
580 }
581 if err := s.tmpl.task.Execute(w, data); err != nil {
582 http.Error(w, err.Error(), http.StatusInternalServerError)
583 return
584 }
585}
586
587type clustersData struct {
588 CurrentPage string
589 Clusters []cluster.State
590}
591
gio59946282024-10-07 12:55:51 +0400592func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400593 clusters, err := s.m.GetClusters()
594 if err != nil {
595 http.Error(w, err.Error(), http.StatusInternalServerError)
596 return
597 }
598 data := clustersData{
599 "clusters",
600 clusters,
601 }
602 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
603 http.Error(w, err.Error(), http.StatusInternalServerError)
604 return
605 }
606}
607
608type clusterData struct {
609 CurrentPage string
610 Cluster cluster.State
611}
612
gio59946282024-10-07 12:55:51 +0400613func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400614 name, ok := mux.Vars(r)["name"]
615 if !ok {
616 http.Error(w, "empty name", http.StatusBadRequest)
617 return
618 }
619 m, err := s.getClusterManager(name)
620 if err != nil {
621 if errors.Is(err, installer.ErrorNotFound) {
622 http.Error(w, "not found", http.StatusNotFound)
623 } else {
624 http.Error(w, err.Error(), http.StatusInternalServerError)
625 }
626 return
627 }
628 data := clusterData{
629 "clusters",
630 m.State(),
631 }
632 if err := s.tmpl.cluster.Execute(w, data); err != nil {
633 http.Error(w, err.Error(), http.StatusInternalServerError)
634 return
635 }
636}
637
gio59946282024-10-07 12:55:51 +0400638func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400639 cName, ok := mux.Vars(r)["name"]
640 if !ok {
641 http.Error(w, "empty name", http.StatusBadRequest)
642 return
643 }
gio8c876172024-10-05 12:25:13 +0400644 tid := 0
645 if t, ok := s.tasks[cName]; ok {
646 if t.task != nil {
647 http.Error(w, "cluster task in progress", http.StatusLocked)
648 return
649 }
650 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400651 }
652 m, err := s.getClusterManager(cName)
653 if err != nil {
654 if errors.Is(err, installer.ErrorNotFound) {
655 http.Error(w, "not found", http.StatusNotFound)
656 } else {
657 http.Error(w, err.Error(), http.StatusInternalServerError)
658 }
659 return
660 }
661 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400662 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400663 go task.Start()
gio8c876172024-10-05 12:25:13 +0400664 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400665 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
666}
667
gio59946282024-10-07 12:55:51 +0400668func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400669 s.l.Lock()
670 defer s.l.Unlock()
671 cName, ok := mux.Vars(r)["cluster"]
672 if !ok {
673 http.Error(w, "empty name", http.StatusBadRequest)
674 return
675 }
gio8c876172024-10-05 12:25:13 +0400676 tid := 0
677 if t, ok := s.tasks[cName]; ok {
678 if t.task != nil {
679 http.Error(w, "cluster task in progress", http.StatusLocked)
680 return
681 }
682 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400683 }
684 sName, ok := mux.Vars(r)["server"]
685 if !ok {
686 http.Error(w, "empty name", http.StatusBadRequest)
687 return
688 }
689 m, err := s.getClusterManager(cName)
690 if err != nil {
691 if errors.Is(err, installer.ErrorNotFound) {
692 http.Error(w, "not found", http.StatusNotFound)
693 } else {
694 http.Error(w, err.Error(), http.StatusInternalServerError)
695 }
696 return
697 }
698 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400699 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400700 go task.Start()
gio8c876172024-10-05 12:25:13 +0400701 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400702 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
703}
704
gio59946282024-10-07 12:55:51 +0400705func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400706 clusters, err := s.m.GetClusters()
707 if err != nil {
708 return nil, err
709 }
710 var c *cluster.State
711 for _, i := range clusters {
712 if i.Name == cName {
713 c = &i
714 break
715 }
716 }
717 if c == nil {
718 return nil, installer.ErrorNotFound
719 }
720 return cluster.RestoreKubeManager(*c)
721}
722
gio59946282024-10-07 12:55:51 +0400723func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400724 s.l.Lock()
725 defer s.l.Unlock()
726 cName, ok := mux.Vars(r)["cluster"]
727 if !ok {
728 http.Error(w, "empty name", http.StatusBadRequest)
729 return
730 }
gio8c876172024-10-05 12:25:13 +0400731 tid := 0
732 if t, ok := s.tasks[cName]; ok {
733 if t.task != nil {
734 http.Error(w, "cluster task in progress", http.StatusLocked)
735 return
736 }
737 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400738 }
739 m, err := s.getClusterManager(cName)
740 if err != nil {
741 if errors.Is(err, installer.ErrorNotFound) {
742 http.Error(w, "not found", http.StatusNotFound)
743 } else {
744 http.Error(w, err.Error(), http.StatusInternalServerError)
745 }
746 return
747 }
748 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400749 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400750 if ip == nil {
751 http.Error(w, "invalid ip", http.StatusBadRequest)
752 return
753 }
754 port := 22
755 if p := r.PostFormValue("port"); p != "" {
756 port, err = strconv.Atoi(p)
757 if err != nil {
758 http.Error(w, err.Error(), http.StatusBadRequest)
759 return
760 }
761 }
762 server := cluster.Server{
763 IP: ip,
764 Port: port,
765 User: r.PostFormValue("user"),
766 Password: r.PostFormValue("password"),
767 }
768 var task tasks.Task
769 switch strings.ToLower(t) {
770 case "controller":
771 if len(m.State().Controllers) == 0 {
772 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
773 } else {
774 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
775 }
776 case "worker":
777 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
778 default:
779 http.Error(w, "invalid type", http.StatusBadRequest)
780 return
781 }
gio8c876172024-10-05 12:25:13 +0400782 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400783 go task.Start()
gio8c876172024-10-05 12:25:13 +0400784 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400785 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
786}
787
gio59946282024-10-07 12:55:51 +0400788func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400789 cName := r.PostFormValue("name")
790 if cName == "" {
791 http.Error(w, "no name", http.StatusBadRequest)
792 return
793 }
794 st := cluster.State{Name: cName}
795 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
796 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
797 return "", err
798 }
799 return fmt.Sprintf("create cluster: %s", cName), nil
800 }); err != nil {
801 http.Error(w, err.Error(), http.StatusInternalServerError)
802 return
803 }
804 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
805}
806
gio59946282024-10-07 12:55:51 +0400807func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400808 cName, ok := mux.Vars(r)["name"]
809 if !ok {
810 http.Error(w, "empty name", http.StatusBadRequest)
811 return
812 }
gio8c876172024-10-05 12:25:13 +0400813 tid := 0
814 if t, ok := s.tasks[cName]; ok {
815 if t.task != nil {
816 http.Error(w, "cluster task in progress", http.StatusLocked)
817 return
818 }
819 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400820 }
821 m, err := s.getClusterManager(cName)
822 if err != nil {
823 if errors.Is(err, installer.ErrorNotFound) {
824 http.Error(w, "not found", http.StatusNotFound)
825 } else {
826 http.Error(w, err.Error(), http.StatusInternalServerError)
827 }
828 return
829 }
830 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400831 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400832 go task.Start()
gio8c876172024-10-05 12:25:13 +0400833 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400834 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
835}
836
gio59946282024-10-07 12:55:51 +0400837func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400838 const vpnUser = "private-network-proxy"
839 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
840 hostname := fmt.Sprintf("cluster-%s", name)
841 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
842 app, err := installer.FindEnvApp(s.fr, "cluster-network")
843 if err != nil {
844 return installer.ReleaseResources{}, err
845 }
846 env, err := s.m.Config()
847 if err != nil {
848 return installer.ReleaseResources{}, err
849 }
850 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
851 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400852 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400853 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
854 "cluster": map[string]any{
855 "name": name,
856 "kubeconfig": kubeconfig,
857 "ingressClassName": ingressClassName,
858 },
859 // TODO(gio): remove hardcoded user
860 "vpnUser": vpnUser,
861 "vpnProxyHostname": hostname,
862 })
863 if err != nil {
864 return installer.ReleaseResources{}, err
865 }
866 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
867 go s.reconciler.Reconcile(ctx)
868 return rr, err
869 })
870 ch := make(chan error)
871 t.OnDone(func(err error) {
872 ch <- err
873 })
874 go t.Start()
875 err := <-ch
876 if err != nil {
877 return nil, err
878 }
879 for {
880 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
881 if err == nil {
882 return ip, nil
883 }
884 if errors.Is(err, installer.ErrorNotFound) {
885 time.Sleep(5 * time.Second)
886 }
887 }
888 }
889}
gio8f290322024-09-21 15:37:45 +0400890
gio59946282024-10-07 12:55:51 +0400891func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +0400892 return func(cm cluster.Manager) error {
893 name := cm.State().Name
894 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
895 app, err := installer.FindEnvApp(s.fr, "longhorn")
896 if err != nil {
897 return installer.ReleaseResources{}, err
898 }
899 env, err := s.m.Config()
900 if err != nil {
901 return installer.ReleaseResources{}, err
902 }
903 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
904 appDir := fmt.Sprintf("/clusters/%s/storage", name)
905 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
906 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
907 "cluster": name,
908 })
909 if err != nil {
910 return installer.ReleaseResources{}, err
911 }
912 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
913 go s.reconciler.Reconcile(ctx)
914 return rr, err
915 })
916 ch := make(chan error)
917 t.OnDone(func(err error) {
918 ch <- err
919 })
920 go t.Start()
921 err := <-ch
922 if err != nil {
923 return err
924 }
925 cm.EnableStorage()
926 return nil
927 }
928}
gio8c876172024-10-05 12:25:13 +0400929
gio59946282024-10-07 12:55:51 +0400930func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +0400931 return func(err error) {
932 if err != nil {
933 fmt.Printf("Task %s failed: %s", name, err.Error())
934 }
935 s.l.Lock()
936 defer s.l.Unlock()
937 s.tasks[name].task = nil
938 go func() {
939 time.Sleep(30 * time.Second)
940 s.l.Lock()
941 defer s.l.Unlock()
942 if t, ok := s.tasks[name]; ok && t.id == id {
943 delete(s.tasks, name)
944 }
945 }()
946 }
947}