blob: 9a15c6e7f5d03289157be1ac52510cfcad8a80c0 [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)
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
115type app struct {
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400116 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 Lekveishvili7efe22f2023-05-30 13:01:53 +0400122}
123
124func (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 Lekveishvili03ee5852023-05-30 13:20:10 +0400131 config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400132 resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config}
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400133 }
134 return c.JSON(http.StatusOK, resp)
135}
136
137func (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 Lekveishvili03ee5852023-05-30 13:20:10 +0400143 config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400144 return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]})
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400145}
146
147type file struct {
148 Name string `json:"name"`
149 Contents string `json:"contents"`
150}
151
152type rendered struct {
153 Readme string `json:"readme"`
154 Files []file `json:"files"`
155}
156
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400157type network struct {
158 Name string
159 IngressClass string
160 CertificateIssuer string
161 Domain string
162}
163
164func 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 Lekveishvili7efe22f2023-05-30 13:01:53 +0400180func (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 Lekveishvili27b2b572023-06-30 10:44:45 +0400194 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 Lekveishvili7efe22f2023-05-30 13:01:53 +0400201 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 Lekveishvili7fb28bf2023-06-24 19:51:16 +0400215 for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400216 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
235func (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 Lekveishvili7fb28bf2023-06-24 19:51:16 +0400249 config, err := s.m.Config()
250 if err != nil {
251 return err
252 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400253 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 Lekveishvili7fb28bf2023-06-24 19:51:16 +0400260 nsGen := installer.NewCombine(
261 installer.NewPrefixGenerator(config.Values.Id+"-"),
262 installer.NewRandomSuffixGenerator(3),
263 )
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400264 if err := s.m.Install(a.App, nsGen, values); err != nil {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400265 return err
266 }
267 return c.String(http.StatusOK, "Installed")
268}
Giorgi Lekveishvili8fe056b2023-06-23 12:01:43 +0400269
270func 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
279func 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}