| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "io/ioutil" |
| 8 | "log" |
| Giorgi Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 9 | "net" |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 10 | "net/http" |
| 11 | "net/http/httputil" |
| 12 | "net/url" |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 13 | "os" |
| 14 | |
| Giorgi Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 15 | "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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 19 | "github.com/labstack/echo/v4" |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 20 | "github.com/spf13/cobra" |
| 21 | "golang.org/x/crypto/ssh" |
| Giorgi Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 22 | |
| 23 | "github.com/giolekva/pcloud/core/installer" |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 24 | ) |
| 25 | |
| 26 | var appManagerFlags struct { |
| 27 | sshKey string |
| 28 | repoAddr string |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 29 | port int |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 30 | } |
| 31 | |
| 32 | func appManagerCmd() *cobra.Command { |
| 33 | cmd := &cobra.Command{ |
| 34 | Use: "appmanager", |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 35 | RunE: appManagerCmdRun, |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 36 | } |
| 37 | cmd.Flags().StringVar( |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 38 | &appManagerFlags.sshKey, |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 39 | "ssh-key", |
| 40 | "", |
| 41 | "", |
| 42 | ) |
| 43 | cmd.Flags().StringVar( |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 44 | &appManagerFlags.repoAddr, |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 45 | "repo-addr", |
| 46 | "", |
| 47 | "", |
| 48 | ) |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 49 | cmd.Flags().IntVar( |
| 50 | &appManagerFlags.port, |
| 51 | "port", |
| 52 | 8080, |
| 53 | "", |
| 54 | ) |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 55 | return cmd |
| 56 | } |
| 57 | |
| 58 | func appManagerCmdRun(cmd *cobra.Command, args []string) error { |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 59 | sshKey, err := os.ReadFile(appManagerFlags.sshKey) |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 60 | if err != nil { |
| 61 | return err |
| 62 | } |
| 63 | signer, err := ssh.ParsePrivateKey(sshKey) |
| 64 | if err != nil { |
| 65 | return err |
| 66 | } |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 67 | repo, err := cloneRepo(appManagerFlags.repoAddr, signer) |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 68 | if err != nil { |
| 69 | return err |
| 70 | } |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 71 | kube, err := newNSCreator() |
| Giorgi Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 72 | if err != nil { |
| 73 | return err |
| 74 | } |
| 75 | m, err := installer.NewAppManager( |
| 76 | installer.NewRepoIO(repo, signer), |
| 77 | kube, |
| 78 | ) |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 79 | if err != nil { |
| 80 | return err |
| 81 | } |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 82 | r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps()) |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 83 | s := &server{ |
| 84 | port: appManagerFlags.port, |
| 85 | m: m, |
| 86 | r: r, |
| 87 | } |
| 88 | s.start() |
| Giorgi Lekveishvili | bd6be7f | 2023-05-26 15:51:28 +0400 | [diff] [blame] | 89 | return nil |
| 90 | } |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 91 | |
| 92 | type server struct { |
| 93 | port int |
| 94 | m *installer.AppManager |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 95 | r installer.AppRepository[installer.StoreApp] |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 96 | } |
| 97 | |
| 98 | func (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) |
| 104 | webapp, err := url.Parse("http://localhost:5173") |
| 105 | if err != nil { |
| 106 | panic(err) |
| 107 | } |
| 108 | // var f ff |
| 109 | e.Any("/*", echo.WrapHandler(httputil.NewSingleHostReverseProxy(webapp))) |
| 110 | // e.Any("/*", echo.WrapHandler(&f)) |
| 111 | fmt.Printf("Starting HTTP server on port: %d\n", s.port) |
| 112 | log.Fatal(e.Start(fmt.Sprintf(":%d", s.port))) |
| 113 | } |
| 114 | |
| 115 | type app struct { |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 116 | Name string `json:"name"` |
| 117 | Icon string `json:"icon"` |
| 118 | ShortDescription string `json:"shortDescription"` |
| 119 | Slug string `json:"slug"` |
| 120 | Schema string `json:"schema"` |
| 121 | Config any `json:"config"` |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 122 | } |
| 123 | |
| 124 | func (s *server) handleAppRepo(c echo.Context) error { |
| 125 | all, err := s.r.GetAll() |
| 126 | if err != nil { |
| 127 | return err |
| 128 | } |
| 129 | resp := make([]app, len(all)) |
| 130 | for i, a := range all { |
| Giorgi Lekveishvili | 03ee585 | 2023-05-30 13:20:10 +0400 | [diff] [blame] | 131 | config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 132 | resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config} |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 133 | } |
| 134 | return c.JSON(http.StatusOK, resp) |
| 135 | } |
| 136 | |
| 137 | func (s *server) handleApp(c echo.Context) error { |
| 138 | slug := c.Param("slug") |
| 139 | a, err := s.r.Find(slug) |
| 140 | if err != nil { |
| 141 | return err |
| 142 | } |
| Giorgi Lekveishvili | 03ee585 | 2023-05-30 13:20:10 +0400 | [diff] [blame] | 143 | config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 144 | return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]}) |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 145 | } |
| 146 | |
| 147 | type file struct { |
| 148 | Name string `json:"name"` |
| 149 | Contents string `json:"contents"` |
| 150 | } |
| 151 | |
| 152 | type rendered struct { |
| 153 | Readme string `json:"readme"` |
| 154 | Files []file `json:"files"` |
| 155 | } |
| 156 | |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 157 | type network struct { |
| 158 | Name string |
| 159 | IngressClass string |
| 160 | CertificateIssuer string |
| 161 | Domain string |
| 162 | } |
| 163 | |
| 164 | func createNetworks(global installer.Config) []network { |
| 165 | return []network{ |
| 166 | { |
| 167 | Name: "Public", |
| 168 | IngressClass: fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName), |
| 169 | CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id), |
| 170 | Domain: global.Values.Domain, |
| 171 | }, |
| 172 | { |
| 173 | Name: "Private", |
| 174 | IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id), |
| 175 | Domain: global.Values.PrivateDomain, |
| 176 | }, |
| 177 | } |
| 178 | } |
| 179 | |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 180 | func (s *server) handleAppRender(c echo.Context) error { |
| 181 | slug := c.Param("slug") |
| 182 | contents, err := ioutil.ReadAll(c.Request().Body) |
| 183 | if err != nil { |
| 184 | return err |
| 185 | } |
| 186 | global, err := s.m.Config() |
| 187 | if err != nil { |
| 188 | return err |
| 189 | } |
| 190 | var values map[string]any |
| 191 | if err := json.Unmarshal(contents, &values); err != nil { |
| 192 | return err |
| 193 | } |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 194 | if network, ok := values["Network"]; ok { |
| 195 | for _, n := range createNetworks(global) { |
| 196 | if n.Name == network { // TODO(giolekva): handle not found |
| 197 | values["Network"] = n |
| 198 | } |
| 199 | } |
| 200 | } |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 201 | all := map[string]any{ |
| 202 | "Global": global.Values, |
| 203 | "Values": values, |
| 204 | } |
| 205 | a, err := s.r.Find(slug) |
| 206 | if err != nil { |
| 207 | return err |
| 208 | } |
| 209 | var readme bytes.Buffer |
| 210 | if err := a.Readme.Execute(&readme, all); err != nil { |
| 211 | return err |
| 212 | } |
| 213 | var resp rendered |
| 214 | resp.Readme = readme.String() |
| Giorgi Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 215 | for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 216 | var f bytes.Buffer |
| 217 | if err := tmpl.Execute(&f, all); err != nil { |
| 218 | fmt.Printf("%+v\n", all) |
| 219 | fmt.Println(err.Error()) |
| 220 | return err |
| 221 | } else { |
| 222 | resp.Files = append(resp.Files, file{tmpl.Name(), f.String()}) |
| 223 | } |
| 224 | } |
| 225 | out, err := json.Marshal(resp) |
| 226 | if err != nil { |
| 227 | return err |
| 228 | } |
| 229 | if _, err := c.Response().Writer.Write(out); err != nil { |
| 230 | return err |
| 231 | } |
| 232 | return nil |
| 233 | } |
| 234 | |
| 235 | func (s *server) handleAppInstall(c echo.Context) error { |
| 236 | slug := c.Param("slug") |
| 237 | contents, err := ioutil.ReadAll(c.Request().Body) |
| 238 | if err != nil { |
| 239 | return err |
| 240 | } |
| 241 | var values map[string]any |
| 242 | if err := json.Unmarshal(contents, &values); err != nil { |
| 243 | return err |
| 244 | } |
| 245 | a, err := s.r.Find(slug) |
| 246 | if err != nil { |
| 247 | return err |
| 248 | } |
| Giorgi Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 249 | config, err := s.m.Config() |
| 250 | if err != nil { |
| 251 | return err |
| 252 | } |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 253 | if network, ok := values["Network"]; ok { |
| 254 | for _, n := range createNetworks(config) { |
| 255 | if n.Name == network { // TODO(giolekva): handle not found |
| 256 | values["Network"] = n |
| 257 | } |
| 258 | } |
| 259 | } |
| Giorgi Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 260 | nsGen := installer.NewCombine( |
| 261 | installer.NewPrefixGenerator(config.Values.Id+"-"), |
| 262 | installer.NewRandomSuffixGenerator(3), |
| 263 | ) |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame^] | 264 | if err := s.m.Install(a.App, nsGen, values); err != nil { |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 265 | return err |
| 266 | } |
| 267 | return c.String(http.StatusOK, "Installed") |
| 268 | } |
| Giorgi Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 269 | |
| 270 | func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) { |
| 271 | return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ |
| 272 | URL: address, |
| 273 | Auth: auth(signer), |
| 274 | RemoteName: "origin", |
| 275 | InsecureSkipTLS: true, |
| 276 | }) |
| 277 | } |
| 278 | |
| 279 | func auth(signer ssh.Signer) *gitssh.PublicKeys { |
| 280 | return &gitssh.PublicKeys{ |
| 281 | Signer: signer, |
| 282 | HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{ |
| 283 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { |
| 284 | // TODO(giolekva): verify server public key |
| 285 | // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key)) |
| 286 | return nil |
| 287 | }, |
| 288 | }, |
| 289 | } |
| 290 | } |