blob: 90f2c4b4ca74f4be04dcdbe88a374f14aec15918 [file] [log] [blame]
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +04001package main
2
3import (
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +04004 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "log"
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +04009 "net"
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040010 "net/http"
11 "net/http/httputil"
12 "net/url"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040013 "os"
14
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +040015 "github.com/go-git/go-billy/v5/memfs"
16 "github.com/go-git/go-git/v5"
17 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
18 "github.com/go-git/go-git/v5/storage/memory"
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040019 "github.com/labstack/echo/v4"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040020 "github.com/spf13/cobra"
21 "golang.org/x/crypto/ssh"
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +040022
23 "github.com/giolekva/pcloud/core/installer"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040024)
25
26var appManagerFlags struct {
27 sshKey string
28 repoAddr string
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040029 port int
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040030}
31
32func appManagerCmd() *cobra.Command {
33 cmd := &cobra.Command{
34 Use: "appmanager",
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040035 RunE: appManagerCmdRun,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040036 }
37 cmd.Flags().StringVar(
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040038 &appManagerFlags.sshKey,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040039 "ssh-key",
40 "",
41 "",
42 )
43 cmd.Flags().StringVar(
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040044 &appManagerFlags.repoAddr,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040045 "repo-addr",
46 "",
47 "",
48 )
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040049 cmd.Flags().IntVar(
50 &appManagerFlags.port,
51 "port",
52 8080,
53 "",
54 )
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040055 return cmd
56}
57
58func appManagerCmdRun(cmd *cobra.Command, args []string) error {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040059 sshKey, err := os.ReadFile(appManagerFlags.sshKey)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040060 if err != nil {
61 return err
62 }
63 signer, err := ssh.ParsePrivateKey(sshKey)
64 if err != nil {
65 return err
66 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040067 repo, err := cloneRepo(appManagerFlags.repoAddr, signer)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040068 if err != nil {
69 return err
70 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +040071 kube, err := newNSCreator()
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040072 if err != nil {
73 return err
74 }
75 m, err := installer.NewAppManager(
76 installer.NewRepoIO(repo, signer),
77 kube,
78 )
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040079 if err != nil {
80 return err
81 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +040082 r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040083 s := &server{
84 port: appManagerFlags.port,
85 m: m,
86 r: r,
87 }
88 s.start()
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040089 return nil
90}
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040091
92type server struct {
93 port int
94 m *installer.AppManager
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +040095 r installer.AppRepository[installer.StoreApp]
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040096}
97
98func (s *server) start() {
99 e := echo.New()
100 e.GET("/api/app-repo", s.handleAppRepo)
101 e.POST("/api/app/:slug/render", s.handleAppRender)
102 e.POST("/api/app/:slug/install", s.handleAppInstall)
103 e.GET("/api/app/:slug", s.handleApp)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400104 e.GET("/api/instance/:slug", s.handleInstance)
105 e.POST("/api/instance/:slug/update", s.handleAppUpdate)
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400106 webapp, err := url.Parse("http://localhost:5173")
107 if err != nil {
108 panic(err)
109 }
110 // var f ff
111 e.Any("/*", echo.WrapHandler(httputil.NewSingleHostReverseProxy(webapp)))
112 // e.Any("/*", echo.WrapHandler(&f))
113 fmt.Printf("Starting HTTP server on port: %d\n", s.port)
114 log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
115}
116
117type app struct {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400118 Name string `json:"name"`
119 Icon string `json:"icon"`
120 ShortDescription string `json:"shortDescription"`
121 Slug string `json:"slug"`
122 Schema string `json:"schema"`
123 Instances []installer.AppConfig `json:"instances,omitempty"`
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400124}
125
126func (s *server) handleAppRepo(c echo.Context) error {
127 all, err := s.r.GetAll()
128 if err != nil {
129 return err
130 }
131 resp := make([]app, len(all))
132 for i, a := range all {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400133 resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400134 }
135 return c.JSON(http.StatusOK, resp)
136}
137
138func (s *server) handleApp(c echo.Context) error {
139 slug := c.Param("slug")
140 a, err := s.r.Find(slug)
141 if err != nil {
142 return err
143 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400144 instances, err := s.m.FindAllInstances(slug)
145 if err != nil {
146 return err
147 }
148 return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, instances})
149}
150
151func (s *server) handleInstance(c echo.Context) error {
152 slug := c.Param("slug")
153 instance, err := s.m.FindInstance(slug)
154 if err != nil {
155 return err
156 }
157 values, ok := instance.Config["Values"].(map[string]any)
158 if !ok {
159 return fmt.Errorf("Expected map")
160 }
161 for k, v := range values {
162 if k == "Network" {
163 n, ok := v.(map[string]any)
164 if !ok {
165 return fmt.Errorf("Expected map")
166 }
167 values["Network"], ok = n["Name"]
168 if !ok {
169 return fmt.Errorf("Missing Name")
170 }
171 break
172 }
173
174 }
175 a, err := s.r.Find(instance.Id)
176 if err != nil {
177 return err
178 }
179 return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, []installer.AppConfig{instance}})
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400180}
181
182type file struct {
183 Name string `json:"name"`
184 Contents string `json:"contents"`
185}
186
187type rendered struct {
188 Readme string `json:"readme"`
189 Files []file `json:"files"`
190}
191
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400192type network struct {
193 Name string
194 IngressClass string
195 CertificateIssuer string
196 Domain string
197}
198
199func createNetworks(global installer.Config) []network {
200 return []network{
201 {
202 Name: "Public",
203 IngressClass: fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName),
204 CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id),
205 Domain: global.Values.Domain,
206 },
207 {
208 Name: "Private",
209 IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
210 Domain: global.Values.PrivateDomain,
211 },
212 }
213}
214
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400215func (s *server) handleAppRender(c echo.Context) error {
216 slug := c.Param("slug")
217 contents, err := ioutil.ReadAll(c.Request().Body)
218 if err != nil {
219 return err
220 }
221 global, err := s.m.Config()
222 if err != nil {
223 return err
224 }
225 var values map[string]any
226 if err := json.Unmarshal(contents, &values); err != nil {
227 return err
228 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400229 if network, ok := values["Network"]; ok {
230 for _, n := range createNetworks(global) {
231 if n.Name == network { // TODO(giolekva): handle not found
232 values["Network"] = n
233 }
234 }
235 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400236 all := map[string]any{
237 "Global": global.Values,
238 "Values": values,
239 }
240 a, err := s.r.Find(slug)
241 if err != nil {
242 return err
243 }
244 var readme bytes.Buffer
245 if err := a.Readme.Execute(&readme, all); err != nil {
246 return err
247 }
248 var resp rendered
249 resp.Readme = readme.String()
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400250 for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400251 var f bytes.Buffer
252 if err := tmpl.Execute(&f, all); err != nil {
253 fmt.Printf("%+v\n", all)
254 fmt.Println(err.Error())
255 return err
256 } else {
257 resp.Files = append(resp.Files, file{tmpl.Name(), f.String()})
258 }
259 }
260 out, err := json.Marshal(resp)
261 if err != nil {
262 return err
263 }
264 if _, err := c.Response().Writer.Write(out); err != nil {
265 return err
266 }
267 return nil
268}
269
270func (s *server) handleAppInstall(c echo.Context) error {
271 slug := c.Param("slug")
272 contents, err := ioutil.ReadAll(c.Request().Body)
273 if err != nil {
274 return err
275 }
276 var values map[string]any
277 if err := json.Unmarshal(contents, &values); err != nil {
278 return err
279 }
280 a, err := s.r.Find(slug)
281 if err != nil {
282 return err
283 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400284 config, err := s.m.Config()
285 if err != nil {
286 return err
287 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400288 if network, ok := values["Network"]; ok {
289 for _, n := range createNetworks(config) {
290 if n.Name == network { // TODO(giolekva): handle not found
291 values["Network"] = n
292 }
293 }
294 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400295 nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
296 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
297 if err := s.m.Install(a.App, nsGen, suffixGen, values); err != nil {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400298 return err
299 }
300 return c.String(http.StatusOK, "Installed")
301}
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +0400302
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400303func (s *server) handleAppUpdate(c echo.Context) error {
304 slug := c.Param("slug")
305 appConfig, err := s.m.AppConfig(slug)
306 if err != nil {
307 return err
308 }
309 contents, err := ioutil.ReadAll(c.Request().Body)
310 if err != nil {
311 return err
312 }
313 var values map[string]any
314 if err := json.Unmarshal(contents, &values); err != nil {
315 return err
316 }
317 a, err := s.r.Find(appConfig.Id)
318 if err != nil {
319 return err
320 }
321 config, err := s.m.Config()
322 if err != nil {
323 return err
324 }
325 if network, ok := values["Network"]; ok {
326 for _, n := range createNetworks(config) {
327 if n.Name == network { // TODO(giolekva): handle not found
328 values["Network"] = n
329 }
330 }
331 }
332 if err := s.m.Update(a.App, slug, values); err != nil {
333 return err
334 }
335 return c.String(http.StatusOK, "Installed")
336}
337
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +0400338func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
339 return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
340 URL: address,
341 Auth: auth(signer),
342 RemoteName: "origin",
343 InsecureSkipTLS: true,
344 })
345}
346
347func auth(signer ssh.Signer) *gitssh.PublicKeys {
348 return &gitssh.PublicKeys{
349 Signer: signer,
350 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
351 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
352 // TODO(giolekva): verify server public key
353 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
354 return nil
355 },
356 },
357 }
358}