| 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) |
| Giorgi Lekveishvili | 7695148 | 2023-06-30 23:25:09 +0400 | [diff] [blame] | 104 | e.GET("/api/instance/:slug", s.handleInstance) |
| 105 | e.POST("/api/instance/:slug/update", s.handleAppUpdate) |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 106 | 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 | |
| 117 | type app struct { |
| Giorgi Lekveishvili | 7695148 | 2023-06-30 23:25:09 +0400 | [diff] [blame] | 118 | 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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 124 | } |
| 125 | |
| 126 | func (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 Lekveishvili | 7695148 | 2023-06-30 23:25:09 +0400 | [diff] [blame] | 133 | resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil} |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 134 | } |
| 135 | return c.JSON(http.StatusOK, resp) |
| 136 | } |
| 137 | |
| 138 | func (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 Lekveishvili | 7695148 | 2023-06-30 23:25:09 +0400 | [diff] [blame] | 144 | 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 | |
| 151 | func (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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 180 | } |
| 181 | |
| 182 | type file struct { |
| 183 | Name string `json:"name"` |
| 184 | Contents string `json:"contents"` |
| 185 | } |
| 186 | |
| 187 | type rendered struct { |
| 188 | Readme string `json:"readme"` |
| 189 | Files []file `json:"files"` |
| 190 | } |
| 191 | |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame] | 192 | type network struct { |
| 193 | Name string |
| 194 | IngressClass string |
| 195 | CertificateIssuer string |
| 196 | Domain string |
| 197 | } |
| 198 | |
| 199 | func 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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 215 | func (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 Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame] | 229 | 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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 236 | 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 Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 250 | for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install |
| Giorgi Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 251 | 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 | |
| 270 | func (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 Lekveishvili | 7fb28bf | 2023-06-24 19:51:16 +0400 | [diff] [blame] | 284 | config, err := s.m.Config() |
| 285 | if err != nil { |
| 286 | return err |
| 287 | } |
| Giorgi Lekveishvili | 27b2b57 | 2023-06-30 10:44:45 +0400 | [diff] [blame] | 288 | 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 Lekveishvili | 6e81318 | 2023-06-30 13:45:30 +0400 | [diff] [blame] | 295 | 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 Lekveishvili | 7efe22f | 2023-05-30 13:01:53 +0400 | [diff] [blame] | 298 | return err |
| 299 | } |
| 300 | return c.String(http.StatusOK, "Installed") |
| 301 | } |
| Giorgi Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 302 | |
| Giorgi Lekveishvili | 7695148 | 2023-06-30 23:25:09 +0400 | [diff] [blame] | 303 | func (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 Lekveishvili | 8fe056b | 2023-06-23 12:01:43 +0400 | [diff] [blame] | 338 | func 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 | |
| 347 | func 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 | } |