blob: b26a00116ed49dca260a798a2421fd290007e46b [file] [log] [blame]
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04001package welcome
2
3import (
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +04004 "context"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04005 "embed"
6 "encoding/json"
7 "fmt"
8 "html/template"
9 "io/ioutil"
10 "log"
11 "net/http"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040012 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040013
14 "github.com/Masterminds/sprig/v3"
15 "github.com/labstack/echo/v4"
16
17 "github.com/giolekva/pcloud/core/installer"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040018 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040019)
20
21//go:embed appmanager-tmpl
22var mgrTmpl embed.FS
23
24//go:embed appmanager-tmpl/base.html
25var baseHtmlTmpl string
26
27//go:embed appmanager-tmpl/app.html
28var appHtmlTmpl string
29
30type AppManagerServer struct {
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040031 port int
32 m *installer.AppManager
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040033 r installer.AppRepository
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040034 reconciler tasks.Reconciler
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040035}
36
37func NewAppManagerServer(
38 port int,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040039 m *installer.AppManager,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040040 r installer.AppRepository,
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040041 reconciler tasks.Reconciler,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040042) *AppManagerServer {
43 return &AppManagerServer{
44 port,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040045 m,
46 r,
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040047 reconciler,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040048 }
49}
50
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040051func (s *AppManagerServer) Start() error {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040052 e := echo.New()
53 e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
54 e.GET("/api/app-repo", s.handleAppRepo)
55 e.POST("/api/app/:slug/render", s.handleAppRender)
56 e.POST("/api/app/:slug/install", s.handleAppInstall)
57 e.GET("/api/app/:slug", s.handleApp)
58 e.GET("/api/instance/:slug", s.handleInstance)
59 e.POST("/api/instance/:slug/update", s.handleAppUpdate)
60 e.POST("/api/instance/:slug/remove", s.handleAppRemove)
61 e.GET("/", s.handleIndex)
62 e.GET("/app/:slug", s.handleAppUI)
63 e.GET("/instance/:slug", s.handleInstanceUI)
64 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040065 return e.Start(fmt.Sprintf(":%d", s.port))
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040066}
67
68type app struct {
69 Name string `json:"name"`
70 Icon template.HTML `json:"icon"`
71 ShortDescription string `json:"shortDescription"`
72 Slug string `json:"slug"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040073 Instances []installer.AppConfig `json:"instances,omitempty"`
74}
75
76func (s *AppManagerServer) handleAppRepo(c echo.Context) error {
77 all, err := s.r.GetAll()
78 if err != nil {
79 return err
80 }
81 resp := make([]app, len(all))
82 for i, a := range all {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040083 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Name(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040084 }
85 return c.JSON(http.StatusOK, resp)
86}
87
88func (s *AppManagerServer) handleApp(c echo.Context) error {
89 slug := c.Param("slug")
90 a, err := s.r.Find(slug)
91 if err != nil {
92 return err
93 }
94 instances, err := s.m.FindAllInstances(slug)
95 if err != nil {
96 return err
97 }
98 for _, instance := range instances {
99 values, ok := instance.Config["Values"].(map[string]any)
100 if !ok {
101 return fmt.Errorf("Expected map")
102 }
103 for k, v := range values {
104 if k == "Network" {
105 n, ok := v.(map[string]any)
106 if !ok {
107 return fmt.Errorf("Expected map")
108 }
109 values["Network"], ok = n["Name"]
110 if !ok {
111 return fmt.Errorf("Missing Name")
112 }
113 break
114 }
115 }
116 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400117 return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Name(), instances})
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400118}
119
120func (s *AppManagerServer) handleInstance(c echo.Context) error {
121 slug := c.Param("slug")
122 instance, err := s.m.FindInstance(slug)
123 if err != nil {
124 return err
125 }
126 values, ok := instance.Config["Values"].(map[string]any)
127 if !ok {
128 return fmt.Errorf("Expected map")
129 }
130 for k, v := range values {
131 if k == "Network" {
132 n, ok := v.(map[string]any)
133 if !ok {
134 return fmt.Errorf("Expected map")
135 }
136 values["Network"], ok = n["Name"]
137 if !ok {
138 return fmt.Errorf("Missing Name")
139 }
140 break
141 }
142 }
143 a, err := s.r.Find(instance.AppId)
144 if err != nil {
145 return err
146 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400147 return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Name(), []installer.AppConfig{instance}})
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400148}
149
150type file struct {
151 Name string `json:"name"`
152 Contents string `json:"contents"`
153}
154
155type rendered struct {
156 Readme string `json:"readme"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400157}
158
159func (s *AppManagerServer) handleAppRender(c echo.Context) error {
160 slug := c.Param("slug")
161 contents, err := ioutil.ReadAll(c.Request().Body)
162 if err != nil {
163 return err
164 }
165 global, err := s.m.Config()
166 if err != nil {
167 return err
168 }
169 var values map[string]any
170 if err := json.Unmarshal(contents, &values); err != nil {
171 return err
172 }
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400173 if network, ok := values["network"]; ok {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400174 for _, n := range installer.CreateNetworks(global) {
175 if n.Name == network { // TODO(giolekva): handle not found
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400176 values["network"] = n
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400177 }
178 }
179 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400180 all := installer.Derived{
181 Global: global.Values,
182 Values: values,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400183 }
184 a, err := s.r.Find(slug)
185 if err != nil {
186 return err
187 }
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400188 r, err := a.Render(all)
189 if err != nil {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400190 return err
191 }
192 var resp rendered
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400193 resp.Readme = r.Readme
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400194 out, err := json.Marshal(resp)
195 if err != nil {
196 return err
197 }
198 if _, err := c.Response().Writer.Write(out); err != nil {
199 return err
200 }
201 return nil
202}
203
204func (s *AppManagerServer) handleAppInstall(c echo.Context) error {
205 slug := c.Param("slug")
206 contents, err := ioutil.ReadAll(c.Request().Body)
207 if err != nil {
208 return err
209 }
210 var values map[string]any
211 if err := json.Unmarshal(contents, &values); err != nil {
212 return err
213 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400214 log.Printf("Values: %+v\n", values)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400215 a, err := s.r.Find(slug)
216 if err != nil {
217 return err
218 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400219 log.Printf("Found application: %s\n", slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400220 config, err := s.m.Config()
221 if err != nil {
222 return err
223 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400224 log.Printf("Configuration: %+v\n", config)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400225 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
gio3af43942024-04-16 08:13:50 +0400226 suffix, err := suffixGen.Generate()
227 if err != nil {
228 return err
229 }
230 appDir := fmt.Sprintf("/apps/%s%s", a.Name(), suffix)
231 namespace := fmt.Sprintf("%s%s%s", config.Values.NamespacePrefix, a.Namespace(), suffix)
232 if err := s.m.Install(a, appDir, namespace, values); err != nil {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400233 log.Printf("%s\n", err.Error())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400234 return err
235 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400236 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
237 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400238 return c.String(http.StatusOK, "Installed")
239}
240
241func (s *AppManagerServer) handleAppUpdate(c echo.Context) error {
242 slug := c.Param("slug")
243 appConfig, err := s.m.AppConfig(slug)
244 if err != nil {
245 return err
246 }
247 contents, err := ioutil.ReadAll(c.Request().Body)
248 if err != nil {
249 return err
250 }
251 var values map[string]any
252 if err := json.Unmarshal(contents, &values); err != nil {
253 return err
254 }
255 a, err := s.r.Find(appConfig.AppId)
256 if err != nil {
257 return err
258 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400259 if err := s.m.Update(a, slug, values); err != nil {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400260 return err
261 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400262 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
263 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400264 return c.String(http.StatusOK, "Installed")
265}
266
267func (s *AppManagerServer) handleAppRemove(c echo.Context) error {
268 slug := c.Param("slug")
269 if err := s.m.Remove(slug); err != nil {
270 return err
271 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400272 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
273 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400274 return c.String(http.StatusOK, "Installed")
275}
276
277func (s *AppManagerServer) handleIndex(c echo.Context) error {
278 tmpl, err := template.ParseFS(mgrTmpl, "appmanager-tmpl/base.html", "appmanager-tmpl/index.html")
279 if err != nil {
280 return err
281 }
282 all, err := s.r.GetAll()
283 if err != nil {
284 return err
285 }
286 resp := make([]app, len(all))
287 for i, a := range all {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400288 resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Name(), nil}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400289 }
290 return tmpl.Execute(c.Response(), resp)
291}
292
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400293type appContext struct {
294 App installer.App
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400295 Instance *installer.AppConfig
296 Instances []installer.AppConfig
297 AvailableNetworks []installer.Network
298}
299
300func (s *AppManagerServer) handleAppUI(c echo.Context) error {
301 baseTmpl, err := newTemplate().Parse(baseHtmlTmpl)
302 if err != nil {
303 return err
304 }
305 appTmpl, err := template.Must(baseTmpl.Clone()).Parse(appHtmlTmpl)
306 if err != nil {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400307 return err
308 }
309 global, err := s.m.Config()
310 if err != nil {
311 return err
312 }
313 slug := c.Param("slug")
314 a, err := s.r.Find(slug)
315 if err != nil {
316 return err
317 }
318 instances, err := s.m.FindAllInstances(slug)
319 if err != nil {
320 return err
321 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400322 err = appTmpl.Execute(c.Response(), appContext{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400323 App: a,
324 Instances: instances,
325 AvailableNetworks: installer.CreateNetworks(global),
326 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400327 return err
328}
329
330func (s *AppManagerServer) handleInstanceUI(c echo.Context) error {
331 baseTmpl, err := newTemplate().Parse(baseHtmlTmpl)
332 if err != nil {
333 return err
334 }
335 appTmpl, err := template.Must(baseTmpl.Clone()).Parse(appHtmlTmpl)
336 // tmpl, err := newTemplate().ParseFS(mgrTmpl, "appmanager-tmpl/base.html", "appmanager-tmpl/app.html")
337 if err != nil {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400338 return err
339 }
340 global, err := s.m.Config()
341 if err != nil {
342 return err
343 }
344 slug := c.Param("slug")
345 instance, err := s.m.FindInstance(slug)
346 if err != nil {
347 return err
348 }
349 a, err := s.r.Find(instance.AppId)
350 if err != nil {
351 return err
352 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400353 instances, err := s.m.FindAllInstances(a.Name())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400354 if err != nil {
355 return err
356 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400357 err = appTmpl.Execute(c.Response(), appContext{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400358 App: a,
359 Instance: &instance,
360 Instances: instances,
361 AvailableNetworks: installer.CreateNetworks(global),
362 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400363 return err
364}
365
366func newTemplate() *template.Template {
367 return template.New("base").Funcs(template.FuncMap(sprig.FuncMap()))
368}