blob: be077e0970b529d58260fcd7d7ced99bbbe8c186 [file] [log] [blame]
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04001package welcome
2
3import (
4 "bytes"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +04005 "context"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04006 "embed"
7 "encoding/json"
8 "fmt"
9 "html/template"
10 "io/ioutil"
11 "log"
12 "net/http"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040013 "time"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040014
15 "github.com/Masterminds/sprig/v3"
16 "github.com/labstack/echo/v4"
17
18 "github.com/giolekva/pcloud/core/installer"
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040019 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040020)
21
22//go:embed appmanager-tmpl
23var mgrTmpl embed.FS
24
25//go:embed appmanager-tmpl/base.html
26var baseHtmlTmpl string
27
28//go:embed appmanager-tmpl/app.html
29var appHtmlTmpl string
30
31type AppManagerServer struct {
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040032 port int
33 m *installer.AppManager
34 r installer.AppRepository[installer.StoreApp]
35 reconciler tasks.Reconciler
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040036}
37
38func NewAppManagerServer(
39 port int,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040040 m *installer.AppManager,
41 r installer.AppRepository[installer.StoreApp],
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040042 reconciler tasks.Reconciler,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040043) *AppManagerServer {
44 return &AppManagerServer{
45 port,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040046 m,
47 r,
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +040048 reconciler,
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040049 }
50}
51
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040052func (s *AppManagerServer) Start() error {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040053 e := echo.New()
54 e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
55 e.GET("/api/app-repo", s.handleAppRepo)
56 e.POST("/api/app/:slug/render", s.handleAppRender)
57 e.POST("/api/app/:slug/install", s.handleAppInstall)
58 e.GET("/api/app/:slug", s.handleApp)
59 e.GET("/api/instance/:slug", s.handleInstance)
60 e.POST("/api/instance/:slug/update", s.handleAppUpdate)
61 e.POST("/api/instance/:slug/remove", s.handleAppRemove)
62 e.GET("/", s.handleIndex)
63 e.GET("/app/:slug", s.handleAppUI)
64 e.GET("/instance/:slug", s.handleInstanceUI)
65 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040066 return e.Start(fmt.Sprintf(":%d", s.port))
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040067}
68
69type app struct {
70 Name string `json:"name"`
71 Icon template.HTML `json:"icon"`
72 ShortDescription string `json:"shortDescription"`
73 Slug string `json:"slug"`
74 Schema string `json:"schema"`
75 Instances []installer.AppConfig `json:"instances,omitempty"`
76}
77
78func (s *AppManagerServer) handleAppRepo(c echo.Context) error {
79 all, err := s.r.GetAll()
80 if err != nil {
81 return err
82 }
83 resp := make([]app, len(all))
84 for i, a := range all {
85 resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
86 }
87 return c.JSON(http.StatusOK, resp)
88}
89
90func (s *AppManagerServer) handleApp(c echo.Context) error {
91 slug := c.Param("slug")
92 a, err := s.r.Find(slug)
93 if err != nil {
94 return err
95 }
96 instances, err := s.m.FindAllInstances(slug)
97 if err != nil {
98 return err
99 }
100 for _, instance := range instances {
101 values, ok := instance.Config["Values"].(map[string]any)
102 if !ok {
103 return fmt.Errorf("Expected map")
104 }
105 for k, v := range values {
106 if k == "Network" {
107 n, ok := v.(map[string]any)
108 if !ok {
109 return fmt.Errorf("Expected map")
110 }
111 values["Network"], ok = n["Name"]
112 if !ok {
113 return fmt.Errorf("Missing Name")
114 }
115 break
116 }
117 }
118 }
119 return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, instances})
120}
121
122func (s *AppManagerServer) handleInstance(c echo.Context) error {
123 slug := c.Param("slug")
124 instance, err := s.m.FindInstance(slug)
125 if err != nil {
126 return err
127 }
128 values, ok := instance.Config["Values"].(map[string]any)
129 if !ok {
130 return fmt.Errorf("Expected map")
131 }
132 for k, v := range values {
133 if k == "Network" {
134 n, ok := v.(map[string]any)
135 if !ok {
136 return fmt.Errorf("Expected map")
137 }
138 values["Network"], ok = n["Name"]
139 if !ok {
140 return fmt.Errorf("Missing Name")
141 }
142 break
143 }
144 }
145 a, err := s.r.Find(instance.AppId)
146 if err != nil {
147 return err
148 }
149 return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, []installer.AppConfig{instance}})
150}
151
152type file struct {
153 Name string `json:"name"`
154 Contents string `json:"contents"`
155}
156
157type rendered struct {
158 Readme string `json:"readme"`
159 Files []file `json:"files"`
160}
161
162func (s *AppManagerServer) handleAppRender(c echo.Context) error {
163 slug := c.Param("slug")
164 contents, err := ioutil.ReadAll(c.Request().Body)
165 if err != nil {
166 return err
167 }
168 global, err := s.m.Config()
169 if err != nil {
170 return err
171 }
172 var values map[string]any
173 if err := json.Unmarshal(contents, &values); err != nil {
174 return err
175 }
176 if network, ok := values["Network"]; ok {
177 for _, n := range installer.CreateNetworks(global) {
178 if n.Name == network { // TODO(giolekva): handle not found
179 values["Network"] = n
180 }
181 }
182 }
183 all := map[string]any{
184 "Global": global.Values,
185 "Values": values,
186 }
187 a, err := s.r.Find(slug)
188 if err != nil {
189 return err
190 }
191 var readme bytes.Buffer
192 if err := a.Readme.Execute(&readme, all); err != nil {
193 return err
194 }
195 var resp rendered
196 resp.Readme = readme.String()
197 for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install
198 var f bytes.Buffer
199 if err := tmpl.Execute(&f, all); err != nil {
200 fmt.Printf("%+v\n", all)
201 fmt.Println(err.Error())
202 return err
203 } else {
204 resp.Files = append(resp.Files, file{tmpl.Name(), f.String()})
205 }
206 }
207 out, err := json.Marshal(resp)
208 if err != nil {
209 return err
210 }
211 if _, err := c.Response().Writer.Write(out); err != nil {
212 return err
213 }
214 return nil
215}
216
217func (s *AppManagerServer) handleAppInstall(c echo.Context) error {
218 slug := c.Param("slug")
219 contents, err := ioutil.ReadAll(c.Request().Body)
220 if err != nil {
221 return err
222 }
223 var values map[string]any
224 if err := json.Unmarshal(contents, &values); err != nil {
225 return err
226 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400227 log.Printf("Values: %+v\n", values)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400228 a, err := s.r.Find(slug)
229 if err != nil {
230 return err
231 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400232 log.Printf("Found application: %s\n", slug)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400233 config, err := s.m.Config()
234 if err != nil {
235 return err
236 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400237 log.Printf("Configuration: %+v\n", config)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400238 nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
239 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
240 if err := s.m.Install(a.App, nsGen, suffixGen, values); err != nil {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400241 log.Printf("%s\n", err.Error())
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400242 return err
243 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400244 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
245 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400246 return c.String(http.StatusOK, "Installed")
247}
248
249func (s *AppManagerServer) handleAppUpdate(c echo.Context) error {
250 slug := c.Param("slug")
251 appConfig, err := s.m.AppConfig(slug)
252 if err != nil {
253 return err
254 }
255 contents, err := ioutil.ReadAll(c.Request().Body)
256 if err != nil {
257 return err
258 }
259 var values map[string]any
260 if err := json.Unmarshal(contents, &values); err != nil {
261 return err
262 }
263 a, err := s.r.Find(appConfig.AppId)
264 if err != nil {
265 return err
266 }
267 if err := s.m.Update(a.App, slug, values); err != nil {
268 return err
269 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400270 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
271 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400272 return c.String(http.StatusOK, "Installed")
273}
274
275func (s *AppManagerServer) handleAppRemove(c echo.Context) error {
276 slug := c.Param("slug")
277 if err := s.m.Remove(slug); err != nil {
278 return err
279 }
Giorgi Lekveishvilid2f3dca2023-12-20 09:31:30 +0400280 ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
281 go s.reconciler.Reconcile(ctx)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400282 return c.String(http.StatusOK, "Installed")
283}
284
285func (s *AppManagerServer) handleIndex(c echo.Context) error {
286 tmpl, err := template.ParseFS(mgrTmpl, "appmanager-tmpl/base.html", "appmanager-tmpl/index.html")
287 if err != nil {
288 return err
289 }
290 all, err := s.r.GetAll()
291 if err != nil {
292 return err
293 }
294 resp := make([]app, len(all))
295 for i, a := range all {
296 resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
297 }
298 return tmpl.Execute(c.Response(), resp)
299}
300
301type appContext[T any] struct {
302 App *T
303 Instance *installer.AppConfig
304 Instances []installer.AppConfig
305 AvailableNetworks []installer.Network
306}
307
308func (s *AppManagerServer) handleAppUI(c echo.Context) error {
309 baseTmpl, err := newTemplate().Parse(baseHtmlTmpl)
310 if err != nil {
311 return err
312 }
313 appTmpl, err := template.Must(baseTmpl.Clone()).Parse(appHtmlTmpl)
314 if err != nil {
315 fmt.Println(err)
316 return err
317 }
318 global, err := s.m.Config()
319 if err != nil {
320 return err
321 }
322 slug := c.Param("slug")
323 a, err := s.r.Find(slug)
324 if err != nil {
325 return err
326 }
327 instances, err := s.m.FindAllInstances(slug)
328 if err != nil {
329 return err
330 }
331 err = appTmpl.Execute(c.Response(), appContext[installer.StoreApp]{
332 App: a,
333 Instances: instances,
334 AvailableNetworks: installer.CreateNetworks(global),
335 })
336 fmt.Println(err)
337 return err
338}
339
340func (s *AppManagerServer) handleInstanceUI(c echo.Context) error {
341 baseTmpl, err := newTemplate().Parse(baseHtmlTmpl)
342 if err != nil {
343 return err
344 }
345 appTmpl, err := template.Must(baseTmpl.Clone()).Parse(appHtmlTmpl)
346 // tmpl, err := newTemplate().ParseFS(mgrTmpl, "appmanager-tmpl/base.html", "appmanager-tmpl/app.html")
347 if err != nil {
348 fmt.Println(err)
349 return err
350 }
351 global, err := s.m.Config()
352 if err != nil {
353 return err
354 }
355 slug := c.Param("slug")
356 instance, err := s.m.FindInstance(slug)
357 if err != nil {
358 return err
359 }
360 a, err := s.r.Find(instance.AppId)
361 if err != nil {
362 return err
363 }
364 instances, err := s.m.FindAllInstances(a.Name)
365 if err != nil {
366 return err
367 }
368 err = appTmpl.Execute(c.Response(), appContext[installer.StoreApp]{
369 App: a,
370 Instance: &instance,
371 Instances: instances,
372 AvailableNetworks: installer.CreateNetworks(global),
373 })
374 fmt.Println(err)
375 return err
376}
377
378func newTemplate() *template.Template {
379 return template.New("base").Funcs(template.FuncMap(sprig.FuncMap()))
380}