blob: 91594ce8756fef10532e211626bfd9b5c1f436af [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 Id string `json:"id"`
164 Config map[string]any `json:"config"`
giofc441e32024-11-11 16:26:14 +0400165}
166
167func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
168 var req dodoAppInstallReq
169 // TODO(gio): validate that no internal fields are overridden by request
170 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
171 http.Error(w, err.Error(), http.StatusBadRequest)
172 return
173 }
174 clusters, err := s.m.GetClusters()
175 if err != nil {
176 http.Error(w, err.Error(), http.StatusInternalServerError)
177 return
178 }
179 req.Config["clusters"] = installer.ToAccessConfigs(clusters)
180 var cfg bytes.Buffer
181 if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
182 http.Error(w, err.Error(), http.StatusInternalServerError)
183 return
184 }
185 app, err := installer.NewDodoApp(cfg.Bytes())
186 if err != nil {
187 http.Error(w, err.Error(), http.StatusBadRequest)
188 return
189 }
gioa421b062025-04-21 09:45:04 +0400190 if instanceId, err := s.install(app, map[string]any{}); err != nil {
giofc441e32024-11-11 16:26:14 +0400191 http.Error(w, err.Error(), http.StatusInternalServerError)
192 return
gioa421b062025-04-21 09:45:04 +0400193 } else {
194 fmt.Fprintf(w, "/tasks/%s", instanceId)
giofc441e32024-11-11 16:26:14 +0400195 }
196}
197
gio59946282024-10-07 12:55:51 +0400198func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
giocb34ad22024-07-11 08:01:13 +0400199 env, err := s.m.Config()
200 if err != nil {
201 http.Error(w, err.Error(), http.StatusInternalServerError)
202 return
203 }
204 networks, err := s.m.CreateNetworks(env)
205 if err != nil {
206 http.Error(w, err.Error(), http.StatusInternalServerError)
207 return
208 }
209 if err := json.NewEncoder(w).Encode(networks); err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 }
213}
214
gio59946282024-10-07 12:55:51 +0400215func (s *Server) handleClusters(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400216 clusters, err := s.m.GetClusters()
217 if err != nil {
218 http.Error(w, err.Error(), http.StatusInternalServerError)
219 return
220 }
221 if err := json.NewEncoder(w).Encode(installer.ToAccessConfigs(clusters)); err != nil {
222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
224 }
225}
226
227type proxyPair struct {
228 From string `json:"from"`
229 To string `json:"to"`
230}
231
gio59946282024-10-07 12:55:51 +0400232func (s *Server) handleProxyAdd(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400233 var req proxyPair
234 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
235 http.Error(w, err.Error(), http.StatusBadRequest)
236 return
237 }
gio721c0042025-04-03 11:56:36 +0400238 if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400239 http.Error(w, err.Error(), http.StatusInternalServerError)
240 return
241 }
242}
243
gio59946282024-10-07 12:55:51 +0400244func (s *Server) handleProxyRemove(w http.ResponseWriter, r *http.Request) {
giof15b9da2024-09-19 06:59:16 +0400245 var req proxyPair
246 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
247 http.Error(w, err.Error(), http.StatusBadRequest)
248 return
249 }
gio721c0042025-04-03 11:56:36 +0400250 if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
giof15b9da2024-09-19 06:59:16 +0400251 http.Error(w, err.Error(), http.StatusInternalServerError)
252 return
253 }
254}
255
256type app struct {
257 Name string `json:"name"`
258 Icon template.HTML `json:"icon"`
259 ShortDescription string `json:"shortDescription"`
260 Slug string `json:"slug"`
261 Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
262}
263
gio59946282024-10-07 12:55:51 +0400264func (s *Server) handleAppRepo(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400265 all, err := s.r.GetAll()
266 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400267 http.Error(w, err.Error(), http.StatusInternalServerError)
268 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400269 }
270 resp := make([]app, len(all))
271 for i, a := range all {
gio44f621b2024-04-29 09:44:38 +0400272 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400273 }
gioaa0fcdb2024-06-10 22:19:25 +0400274 w.Header().Set("Content-Type", "application/json")
275 if err := json.NewEncoder(w).Encode(resp); err != nil {
276 http.Error(w, err.Error(), http.StatusInternalServerError)
277 return
278 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400279}
280
gio59946282024-10-07 12:55:51 +0400281func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400282 slug, ok := mux.Vars(r)["slug"]
283 if !ok {
284 http.Error(w, "empty slug", http.StatusBadRequest)
285 return
286 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400287 a, err := s.r.Find(slug)
288 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400289 http.Error(w, err.Error(), http.StatusInternalServerError)
290 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400291 }
gio7fbd4ad2024-08-27 10:06:39 +0400292 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400293 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400294 http.Error(w, err.Error(), http.StatusInternalServerError)
295 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400296 }
gioaa0fcdb2024-06-10 22:19:25 +0400297 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
298 w.Header().Set("Content-Type", "application/json")
299 if err := json.NewEncoder(w).Encode(resp); err != nil {
300 http.Error(w, err.Error(), http.StatusInternalServerError)
301 return
302 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400303}
304
gio59946282024-10-07 12:55:51 +0400305func (s *Server) handleInstance(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400306 slug, ok := mux.Vars(r)["slug"]
307 if !ok {
308 http.Error(w, "empty slug", http.StatusBadRequest)
309 return
310 }
gio7fbd4ad2024-08-27 10:06:39 +0400311 instance, err := s.m.GetInstance(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400312 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400313 http.Error(w, err.Error(), http.StatusInternalServerError)
314 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400315 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400316 a, err := s.r.Find(instance.AppId)
317 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400320 }
gioaa0fcdb2024-06-10 22:19:25 +0400321 resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
322 w.Header().Set("Content-Type", "application/json")
323 if err := json.NewEncoder(w).Encode(resp); err != nil {
324 http.Error(w, err.Error(), http.StatusInternalServerError)
325 return
326 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400327}
328
gioa421b062025-04-21 09:45:04 +0400329func (s *Server) install(app installer.EnvApp, values map[string]any) (string, error) {
330 env, err := s.m.Config()
331 if err != nil {
332 return "", err
333 }
334 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
335 suffix, err := suffixGen.Generate()
336 if err != nil {
337 return "", err
338 }
339 instanceId := app.Slug() + suffix
340 appDir := fmt.Sprintf("/apps/%s", instanceId)
341 namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, app.Namespace(), suffix)
342 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
343 rr, err := s.m.Install(app, instanceId, appDir, namespace, values)
344 if err == nil {
345 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
346 go s.reconciler.Reconcile(ctx)
347 }
348 return rr, err
349 })
350 if _, ok := s.tasks[instanceId]; ok {
351 panic("MUST NOT REACH!")
352 }
353 s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
354 t.OnDone(s.cleanTask(instanceId, 0))
355 go t.Start()
356 return instanceId, nil
357}
358
gio59946282024-10-07 12:55:51 +0400359func (s *Server) handleAppInstall(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400360 s.l.Lock()
361 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400362 slug, ok := mux.Vars(r)["slug"]
363 if !ok {
364 http.Error(w, "empty slug", http.StatusBadRequest)
365 return
366 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400367 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400368 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400369 http.Error(w, err.Error(), http.StatusInternalServerError)
370 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400371 }
gioa421b062025-04-21 09:45:04 +0400372 app, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400373 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400374 http.Error(w, err.Error(), http.StatusInternalServerError)
375 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400376 }
gioa421b062025-04-21 09:45:04 +0400377 if instanceId, err := s.install(app, values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400378 http.Error(w, err.Error(), http.StatusInternalServerError)
379 return
gioa421b062025-04-21 09:45:04 +0400380 } else {
381 fmt.Fprintf(w, "/tasks/%s", instanceId)
gioaa0fcdb2024-06-10 22:19:25 +0400382 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400383}
384
gio59946282024-10-07 12:55:51 +0400385func (s *Server) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400386 s.l.Lock()
387 defer s.l.Unlock()
gioaa0fcdb2024-06-10 22:19:25 +0400388 slug, ok := mux.Vars(r)["slug"]
389 if !ok {
390 http.Error(w, "empty slug", http.StatusBadRequest)
391 return
392 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400393 var values map[string]any
gio8c876172024-10-05 12:25:13 +0400394 if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400397 }
gio8c876172024-10-05 12:25:13 +0400398 tid := 0
399 if t, ok := s.tasks[slug]; ok {
400 if t.task != nil {
401 http.Error(w, "Update already in progress", http.StatusBadRequest)
402 return
403 }
404 tid = t.id + 1
gio778577f2024-04-29 09:44:38 +0400405 }
giof8843412024-05-22 16:38:05 +0400406 rr, err := s.m.Update(slug, values)
gio778577f2024-04-29 09:44:38 +0400407 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400408 http.Error(w, err.Error(), http.StatusInternalServerError)
409 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400410 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400411 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
412 go s.reconciler.Reconcile(ctx)
gio778577f2024-04-29 09:44:38 +0400413 t := tasks.NewMonitorRelease(s.h, rr)
gio8c876172024-10-05 12:25:13 +0400414 t.OnDone(s.cleanTask(slug, tid))
415 s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
gio778577f2024-04-29 09:44:38 +0400416 go t.Start()
giof6ad2982024-08-23 17:42:49 +0400417 if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400421}
422
gio59946282024-10-07 12:55:51 +0400423func (s *Server) handleAppRemove(w http.ResponseWriter, r *http.Request) {
gioaa0fcdb2024-06-10 22:19:25 +0400424 slug, ok := mux.Vars(r)["slug"]
425 if !ok {
426 http.Error(w, "empty slug", http.StatusBadRequest)
427 return
428 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400429 if err := s.m.Remove(slug); err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400430 http.Error(w, err.Error(), http.StatusInternalServerError)
431 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400432 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400433 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
434 go s.reconciler.Reconcile(ctx)
gioaa0fcdb2024-06-10 22:19:25 +0400435 if _, err := fmt.Fprint(w, "/"); err != nil {
436 http.Error(w, err.Error(), http.StatusInternalServerError)
437 return
438 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400439}
440
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400441type PageData struct {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400442 Apps []app
443 CurrentPage string
444 SearchTarget string
445 SearchValue string
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400446}
447
gio59946282024-10-07 12:55:51 +0400448func (s *Server) handleAppsList(w http.ResponseWriter, r *http.Request) {
Davit Tabidze780a0d02024-08-05 20:53:26 +0400449 pageType := mux.Vars(r)["pageType"]
450 if pageType == "" {
451 pageType = "all"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400452 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400453 searchQuery := r.FormValue("query")
454 apps, err := s.r.Filter(searchQuery)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400455 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400456 http.Error(w, err.Error(), http.StatusInternalServerError)
457 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400458 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400459 resp := make([]app, 0)
Davit Tabidze780a0d02024-08-05 20:53:26 +0400460 for _, a := range apps {
gio7fbd4ad2024-08-27 10:06:39 +0400461 instances, err := s.m.GetAllAppInstances(a.Slug())
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400462 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400463 http.Error(w, err.Error(), http.StatusInternalServerError)
464 return
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400465 }
Davit Tabidze780a0d02024-08-05 20:53:26 +0400466 switch pageType {
467 case "installed":
468 if len(instances) != 0 {
469 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
470 }
471 case "not-installed":
472 if len(instances) == 0 {
473 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
474 }
475 default:
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400476 resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
477 }
478 }
479 data := PageData{
Davit Tabidze780a0d02024-08-05 20:53:26 +0400480 Apps: resp,
481 CurrentPage: pageType,
482 SearchTarget: pageType,
483 SearchValue: searchQuery,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400484 }
gioaa0fcdb2024-06-10 22:19:25 +0400485 if err := s.tmpl.index.Execute(w, data); err != nil {
486 http.Error(w, err.Error(), http.StatusInternalServerError)
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400487 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400488}
489
490type appPageData struct {
gio3cdee592024-04-17 10:15:56 +0400491 App installer.EnvApp
492 Instance *installer.AppInstanceConfig
493 Instances []installer.AppInstanceConfig
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400494 AvailableNetworks []installer.Network
giof6ad2982024-08-23 17:42:49 +0400495 AvailableClusters []cluster.State
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400496 CurrentPage string
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400497}
498
gio59946282024-10-07 12:55:51 +0400499func (s *Server) handleAppUI(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400500 global, err := s.m.Config()
501 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400502 http.Error(w, err.Error(), http.StatusInternalServerError)
503 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400504 }
gioaa0fcdb2024-06-10 22:19:25 +0400505 slug, ok := mux.Vars(r)["slug"]
506 if !ok {
507 http.Error(w, "empty slug", http.StatusBadRequest)
508 return
509 }
gio3cdee592024-04-17 10:15:56 +0400510 a, err := installer.FindEnvApp(s.r, slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400511 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400512 http.Error(w, err.Error(), http.StatusInternalServerError)
513 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400514 }
gio7fbd4ad2024-08-27 10:06:39 +0400515 instances, err := s.m.GetAllAppInstances(slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400516 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400517 http.Error(w, err.Error(), http.StatusInternalServerError)
518 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400519 }
giocb34ad22024-07-11 08:01:13 +0400520 networks, err := s.m.CreateNetworks(global)
521 if err != nil {
522 http.Error(w, err.Error(), http.StatusInternalServerError)
523 return
524 }
giof6ad2982024-08-23 17:42:49 +0400525 clusters, err := s.m.GetClusters()
526 if err != nil {
527 http.Error(w, err.Error(), http.StatusInternalServerError)
528 return
529 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400530 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400531 App: a,
532 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400533 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400534 AvailableClusters: clusters,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400535 CurrentPage: a.Name(),
536 }
gioaa0fcdb2024-06-10 22:19:25 +0400537 if err := s.tmpl.app.Execute(w, data); err != nil {
538 http.Error(w, err.Error(), http.StatusInternalServerError)
539 return
540 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400541}
542
gio59946282024-10-07 12:55:51 +0400543func (s *Server) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400544 s.l.Lock()
545 defer s.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400546 global, err := s.m.Config()
547 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400548 http.Error(w, err.Error(), http.StatusInternalServerError)
549 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400550 }
gioaa0fcdb2024-06-10 22:19:25 +0400551 slug, ok := mux.Vars(r)["slug"]
552 if !ok {
553 http.Error(w, "empty slug", http.StatusBadRequest)
554 return
555 }
gio8c876172024-10-05 12:25:13 +0400556 if t, ok := s.tasks[slug]; ok && t.task != nil {
giof6ad2982024-08-23 17:42:49 +0400557 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", slug), http.StatusSeeOther)
558 return
559 }
gio8c876172024-10-05 12:25:13 +0400560 instance, err := s.m.GetInstance(slug)
561 if err != nil {
562 http.Error(w, err.Error(), http.StatusInternalServerError)
563 return
564 }
565 a, err := s.m.GetInstanceApp(instance.Id)
566 if err != nil {
567 http.Error(w, err.Error(), http.StatusInternalServerError)
568 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400569 }
gio7fbd4ad2024-08-27 10:06:39 +0400570 instances, err := s.m.GetAllAppInstances(a.Slug())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 if err != nil {
gioaa0fcdb2024-06-10 22:19:25 +0400572 http.Error(w, err.Error(), http.StatusInternalServerError)
573 return
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400574 }
giocb34ad22024-07-11 08:01:13 +0400575 networks, err := s.m.CreateNetworks(global)
576 if err != nil {
577 http.Error(w, err.Error(), http.StatusInternalServerError)
578 return
579 }
giof6ad2982024-08-23 17:42:49 +0400580 clusters, err := s.m.GetClusters()
581 if err != nil {
582 http.Error(w, err.Error(), http.StatusInternalServerError)
583 return
584 }
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400585 data := appPageData{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400586 App: a,
gio778577f2024-04-29 09:44:38 +0400587 Instance: instance,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400588 Instances: instances,
giocb34ad22024-07-11 08:01:13 +0400589 AvailableNetworks: networks,
giof6ad2982024-08-23 17:42:49 +0400590 AvailableClusters: clusters,
gio1cd65152024-08-16 08:18:49 +0400591 CurrentPage: slug,
Davit Tabidze3ec24cf2024-05-22 14:06:02 +0400592 }
gioaa0fcdb2024-06-10 22:19:25 +0400593 if err := s.tmpl.app.Execute(w, data); err != nil {
594 http.Error(w, err.Error(), http.StatusInternalServerError)
595 return
596 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400597}
giof6ad2982024-08-23 17:42:49 +0400598
599type taskStatusData struct {
600 CurrentPage string
601 Task tasks.Task
602}
603
gio59946282024-10-07 12:55:51 +0400604func (s *Server) handleTaskStatus(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400605 s.l.Lock()
606 defer s.l.Unlock()
607 slug, ok := mux.Vars(r)["slug"]
608 if !ok {
609 http.Error(w, "empty slug", http.StatusBadRequest)
610 return
611 }
612 t, ok := s.tasks[slug]
613 if !ok {
614 http.Error(w, "task not found", http.StatusInternalServerError)
615
616 return
617 }
gio8c876172024-10-05 12:25:13 +0400618 if ok && t.task == nil {
giof6ad2982024-08-23 17:42:49 +0400619 http.Redirect(w, r, t.redirectTo, http.StatusSeeOther)
620 return
621 }
622 data := taskStatusData{
623 CurrentPage: "",
624 Task: t.task,
625 }
626 if err := s.tmpl.task.Execute(w, data); err != nil {
627 http.Error(w, err.Error(), http.StatusInternalServerError)
628 return
629 }
630}
631
632type clustersData struct {
633 CurrentPage string
634 Clusters []cluster.State
635}
636
gio59946282024-10-07 12:55:51 +0400637func (s *Server) handleAllClusters(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400638 clusters, err := s.m.GetClusters()
639 if err != nil {
640 http.Error(w, err.Error(), http.StatusInternalServerError)
641 return
642 }
643 data := clustersData{
644 "clusters",
645 clusters,
646 }
647 if err := s.tmpl.allClusters.Execute(w, data); err != nil {
648 http.Error(w, err.Error(), http.StatusInternalServerError)
649 return
650 }
651}
652
653type clusterData struct {
654 CurrentPage string
655 Cluster cluster.State
656}
657
gio59946282024-10-07 12:55:51 +0400658func (s *Server) handleCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400659 name, ok := mux.Vars(r)["name"]
660 if !ok {
661 http.Error(w, "empty name", http.StatusBadRequest)
662 return
663 }
664 m, err := s.getClusterManager(name)
665 if err != nil {
666 if errors.Is(err, installer.ErrorNotFound) {
667 http.Error(w, "not found", http.StatusNotFound)
668 } else {
669 http.Error(w, err.Error(), http.StatusInternalServerError)
670 }
671 return
672 }
673 data := clusterData{
674 "clusters",
675 m.State(),
676 }
677 if err := s.tmpl.cluster.Execute(w, data); err != nil {
678 http.Error(w, err.Error(), http.StatusInternalServerError)
679 return
680 }
681}
682
gio59946282024-10-07 12:55:51 +0400683func (s *Server) handleClusterSetupStorage(w http.ResponseWriter, r *http.Request) {
gio8f290322024-09-21 15:37:45 +0400684 cName, ok := mux.Vars(r)["name"]
685 if !ok {
686 http.Error(w, "empty name", http.StatusBadRequest)
687 return
688 }
gio8c876172024-10-05 12:25:13 +0400689 tid := 0
690 if t, ok := s.tasks[cName]; ok {
691 if t.task != nil {
692 http.Error(w, "cluster task in progress", http.StatusLocked)
693 return
694 }
695 tid = t.id + 1
gio8f290322024-09-21 15:37:45 +0400696 }
697 m, err := s.getClusterManager(cName)
698 if err != nil {
699 if errors.Is(err, installer.ErrorNotFound) {
700 http.Error(w, "not found", http.StatusNotFound)
701 } else {
702 http.Error(w, err.Error(), http.StatusInternalServerError)
703 }
704 return
705 }
706 task := tasks.NewClusterSetupTask(m, s.setupRemoteClusterStorage(), s.repo, fmt.Sprintf("cluster %s: setting up storage", m.State().Name))
gio8c876172024-10-05 12:25:13 +0400707 task.OnDone(s.cleanTask(cName, tid))
gio8f290322024-09-21 15:37:45 +0400708 go task.Start()
gio8c876172024-10-05 12:25:13 +0400709 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
gio8f290322024-09-21 15:37:45 +0400710 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
711}
712
gio59946282024-10-07 12:55:51 +0400713func (s *Server) handleClusterRemoveServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400714 s.l.Lock()
715 defer s.l.Unlock()
716 cName, ok := mux.Vars(r)["cluster"]
717 if !ok {
718 http.Error(w, "empty name", http.StatusBadRequest)
719 return
720 }
gio8c876172024-10-05 12:25:13 +0400721 tid := 0
722 if t, ok := s.tasks[cName]; ok {
723 if t.task != nil {
724 http.Error(w, "cluster task in progress", http.StatusLocked)
725 return
726 }
727 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400728 }
729 sName, ok := mux.Vars(r)["server"]
730 if !ok {
731 http.Error(w, "empty name", http.StatusBadRequest)
732 return
733 }
734 m, err := s.getClusterManager(cName)
735 if err != nil {
736 if errors.Is(err, installer.ErrorNotFound) {
737 http.Error(w, "not found", http.StatusNotFound)
738 } else {
739 http.Error(w, err.Error(), http.StatusInternalServerError)
740 }
741 return
742 }
743 task := tasks.NewClusterRemoveServerTask(m, sName, s.repo)
gio8c876172024-10-05 12:25:13 +0400744 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400745 go task.Start()
gio8c876172024-10-05 12:25:13 +0400746 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400747 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
748}
749
gio59946282024-10-07 12:55:51 +0400750func (s *Server) getClusterManager(cName string) (cluster.Manager, error) {
giof6ad2982024-08-23 17:42:49 +0400751 clusters, err := s.m.GetClusters()
752 if err != nil {
753 return nil, err
754 }
755 var c *cluster.State
756 for _, i := range clusters {
757 if i.Name == cName {
758 c = &i
759 break
760 }
761 }
762 if c == nil {
763 return nil, installer.ErrorNotFound
764 }
765 return cluster.RestoreKubeManager(*c)
766}
767
gio59946282024-10-07 12:55:51 +0400768func (s *Server) handleClusterAddServer(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400769 s.l.Lock()
770 defer s.l.Unlock()
771 cName, ok := mux.Vars(r)["cluster"]
772 if !ok {
773 http.Error(w, "empty name", http.StatusBadRequest)
774 return
775 }
gio8c876172024-10-05 12:25:13 +0400776 tid := 0
777 if t, ok := s.tasks[cName]; ok {
778 if t.task != nil {
779 http.Error(w, "cluster task in progress", http.StatusLocked)
780 return
781 }
782 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400783 }
784 m, err := s.getClusterManager(cName)
785 if err != nil {
786 if errors.Is(err, installer.ErrorNotFound) {
787 http.Error(w, "not found", http.StatusNotFound)
788 } else {
789 http.Error(w, err.Error(), http.StatusInternalServerError)
790 }
791 return
792 }
793 t := r.PostFormValue("type")
gio8f290322024-09-21 15:37:45 +0400794 ip := net.ParseIP(strings.TrimSpace(r.PostFormValue("ip")))
giof6ad2982024-08-23 17:42:49 +0400795 if ip == nil {
796 http.Error(w, "invalid ip", http.StatusBadRequest)
797 return
798 }
799 port := 22
800 if p := r.PostFormValue("port"); p != "" {
801 port, err = strconv.Atoi(p)
802 if err != nil {
803 http.Error(w, err.Error(), http.StatusBadRequest)
804 return
805 }
806 }
807 server := cluster.Server{
808 IP: ip,
809 Port: port,
810 User: r.PostFormValue("user"),
811 Password: r.PostFormValue("password"),
812 }
813 var task tasks.Task
814 switch strings.ToLower(t) {
815 case "controller":
816 if len(m.State().Controllers) == 0 {
817 task = tasks.NewClusterInitTask(m, server, s.cnc, s.repo, s.setupRemoteCluster())
818 } else {
819 task = tasks.NewClusterJoinControllerTask(m, server, s.repo)
820 }
821 case "worker":
822 task = tasks.NewClusterJoinWorkerTask(m, server, s.repo)
823 default:
824 http.Error(w, "invalid type", http.StatusBadRequest)
825 return
826 }
gio8c876172024-10-05 12:25:13 +0400827 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400828 go task.Start()
gio8c876172024-10-05 12:25:13 +0400829 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400830 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
831}
832
gio59946282024-10-07 12:55:51 +0400833func (s *Server) handleCreateCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400834 cName := r.PostFormValue("name")
835 if cName == "" {
836 http.Error(w, "no name", http.StatusBadRequest)
837 return
838 }
839 st := cluster.State{Name: cName}
840 if _, err := s.repo.Do(func(fs soft.RepoFS) (string, error) {
841 if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", cName), st); err != nil {
842 return "", err
843 }
844 return fmt.Sprintf("create cluster: %s", cName), nil
845 }); err != nil {
846 http.Error(w, err.Error(), http.StatusInternalServerError)
847 return
848 }
849 http.Redirect(w, r, fmt.Sprintf("/clusters/%s", cName), http.StatusSeeOther)
850}
851
gio59946282024-10-07 12:55:51 +0400852func (s *Server) handleRemoveCluster(w http.ResponseWriter, r *http.Request) {
giof6ad2982024-08-23 17:42:49 +0400853 cName, ok := mux.Vars(r)["name"]
854 if !ok {
855 http.Error(w, "empty name", http.StatusBadRequest)
856 return
857 }
gio8c876172024-10-05 12:25:13 +0400858 tid := 0
859 if t, ok := s.tasks[cName]; ok {
860 if t.task != nil {
861 http.Error(w, "cluster task in progress", http.StatusLocked)
862 return
863 }
864 tid = t.id + 1
giof6ad2982024-08-23 17:42:49 +0400865 }
866 m, err := s.getClusterManager(cName)
867 if err != nil {
868 if errors.Is(err, installer.ErrorNotFound) {
869 http.Error(w, "not found", http.StatusNotFound)
870 } else {
871 http.Error(w, err.Error(), http.StatusInternalServerError)
872 }
873 return
874 }
875 task := tasks.NewRemoveClusterTask(m, s.cnc, s.repo)
gio8c876172024-10-05 12:25:13 +0400876 task.OnDone(s.cleanTask(cName, tid))
giof6ad2982024-08-23 17:42:49 +0400877 go task.Start()
gio8c876172024-10-05 12:25:13 +0400878 s.tasks[cName] = &taskForward{task, fmt.Sprintf("/clusters/%s", cName), tid}
giof6ad2982024-08-23 17:42:49 +0400879 http.Redirect(w, r, fmt.Sprintf("/tasks/%s", cName), http.StatusSeeOther)
880}
881
gio59946282024-10-07 12:55:51 +0400882func (s *Server) setupRemoteCluster() cluster.ClusterIngressSetupFunc {
giof6ad2982024-08-23 17:42:49 +0400883 const vpnUser = "private-network-proxy"
884 return func(name, kubeconfig, ingressClassName string) (net.IP, error) {
885 hostname := fmt.Sprintf("cluster-%s", name)
886 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
887 app, err := installer.FindEnvApp(s.fr, "cluster-network")
888 if err != nil {
889 return installer.ReleaseResources{}, err
890 }
891 env, err := s.m.Config()
892 if err != nil {
893 return installer.ReleaseResources{}, err
894 }
gio721c0042025-04-03 11:56:36 +0400895 keys, err := installer.NewSSHKeyPair("port-allocator")
896 if err != nil {
897 return installer.ReleaseResources{}, err
898 }
899 user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
900 if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
901 return installer.ReleaseResources{}, err
902 }
903 if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
904 return installer.ReleaseResources{}, err
905 }
giof6ad2982024-08-23 17:42:49 +0400906 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
907 appDir := fmt.Sprintf("/clusters/%s/ingress", name)
gio8f290322024-09-21 15:37:45 +0400908 namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
giof6ad2982024-08-23 17:42:49 +0400909 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
910 "cluster": map[string]any{
911 "name": name,
912 "kubeconfig": kubeconfig,
913 "ingressClassName": ingressClassName,
914 },
915 // TODO(gio): remove hardcoded user
916 "vpnUser": vpnUser,
917 "vpnProxyHostname": hostname,
gio721c0042025-04-03 11:56:36 +0400918 "sshPrivateKey": string(keys.RawPrivateKey()),
giof6ad2982024-08-23 17:42:49 +0400919 })
920 if err != nil {
921 return installer.ReleaseResources{}, err
922 }
923 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
924 go s.reconciler.Reconcile(ctx)
925 return rr, err
926 })
927 ch := make(chan error)
928 t.OnDone(func(err error) {
929 ch <- err
930 })
931 go t.Start()
932 err := <-ch
933 if err != nil {
934 return nil, err
935 }
936 for {
937 ip, err := s.vpnAPIClient.GetNodeIP(vpnUser, hostname)
938 if err == nil {
939 return ip, nil
940 }
941 if errors.Is(err, installer.ErrorNotFound) {
942 time.Sleep(5 * time.Second)
943 }
944 }
945 }
946}
gio8f290322024-09-21 15:37:45 +0400947
gio59946282024-10-07 12:55:51 +0400948func (s *Server) setupRemoteClusterStorage() cluster.ClusterSetupFunc {
gio8f290322024-09-21 15:37:45 +0400949 return func(cm cluster.Manager) error {
950 name := cm.State().Name
951 t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
952 app, err := installer.FindEnvApp(s.fr, "longhorn")
953 if err != nil {
954 return installer.ReleaseResources{}, err
955 }
956 env, err := s.m.Config()
957 if err != nil {
958 return installer.ReleaseResources{}, err
959 }
960 instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
961 appDir := fmt.Sprintf("/clusters/%s/storage", name)
962 namespace := fmt.Sprintf("%scluster-%s-storage", env.NamespacePrefix, name)
963 rr, err := s.m.Install(app, instanceId, appDir, namespace, map[string]any{
964 "cluster": name,
965 })
966 if err != nil {
967 return installer.ReleaseResources{}, err
968 }
969 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
970 go s.reconciler.Reconcile(ctx)
971 return rr, err
972 })
973 ch := make(chan error)
974 t.OnDone(func(err error) {
975 ch <- err
976 })
977 go t.Start()
978 err := <-ch
979 if err != nil {
980 return err
981 }
982 cm.EnableStorage()
983 return nil
984 }
985}
gio8c876172024-10-05 12:25:13 +0400986
gio59946282024-10-07 12:55:51 +0400987func (s *Server) cleanTask(name string, id int) func(error) {
gio8c876172024-10-05 12:25:13 +0400988 return func(err error) {
989 if err != nil {
990 fmt.Printf("Task %s failed: %s", name, err.Error())
991 }
992 s.l.Lock()
993 defer s.l.Unlock()
994 s.tasks[name].task = nil
995 go func() {
996 time.Sleep(30 * time.Second)
997 s.l.Lock()
998 defer s.l.Unlock()
999 if t, ok := s.tasks[name]; ok && t.id == id {
1000 delete(s.tasks, name)
1001 }
1002 }()
1003 }
1004}