blob: 4279445cb01182b985e0b8616a808afc8c9b62db [file] [log] [blame]
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04001package main
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "log"
8 "os"
9 "path"
10 "text/template"
11
12 "github.com/labstack/echo/v4"
13 "github.com/spf13/cobra"
14
15 "github.com/giolekva/pcloud/core/installer"
16 "github.com/giolekva/pcloud/core/installer/soft"
17)
18
19var envManagerFlags struct {
20 repoIP string
21 repoPort int
22 sshKey string
23 port int
24}
25
26func envManagerCmd() *cobra.Command {
27 cmd := &cobra.Command{
28 Use: "envmanager",
29 RunE: envManagerCmdRun,
30 }
31 cmd.Flags().StringVar(
32 &envManagerFlags.repoIP,
33 "repo-ip",
34 "",
35 "",
36 )
37 cmd.Flags().IntVar(
38 &envManagerFlags.repoPort,
39 "repo-port",
40 22,
41 "",
42 )
43 cmd.Flags().StringVar(
44 &envManagerFlags.sshKey,
45 "ssh-key",
46 "",
47 "",
48 )
49 cmd.Flags().IntVar(
50 &envManagerFlags.port,
51 "port",
52 8080,
53 "",
54 )
55 return cmd
56}
57
58func envManagerCmdRun(cmd *cobra.Command, args []string) error {
59 sshKey, err := os.ReadFile(envManagerFlags.sshKey)
60 if err != nil {
61 return err
62 }
63 fmt.Println(string(sshKey))
64 ss, err := soft.NewClient(envManagerFlags.repoIP, envManagerFlags.repoPort, sshKey, log.Default())
65 if err != nil {
66 return err
67 }
68 b, err := ss.GetPublicKey()
69 if err != nil {
70 return err
71 }
72 fmt.Println(string(b))
73 fmt.Println(111)
74 repo, err := ss.GetRepo("pcloud")
75 fmt.Println(222)
76 if err != nil {
77 return err
78 }
79 fmt.Println(333)
80 repoIO := installer.NewRepoIO(repo, ss.Signer)
81 s := &envServer{
82 port: envManagerFlags.port,
83 ss: ss,
84 repo: repoIO,
85 }
86 s.start()
87 return nil
88}
89
90type envServer struct {
91 port int
92 ss *soft.Client
93 repo installer.RepoIO
94}
95
96func (s *envServer) start() {
97 e := echo.New()
98 e.POST("/env", s.createEnv)
99 log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
100}
101
102type createEnvReq struct {
103 Name string `json:"name"`
104 ContactEmail string `json:"contactEmail"`
105 Domain string `json:"domain"`
106 GandiAPIToken string `json:"gandiAPIToken"`
107 AdminUsername string `json:"adminUsername"`
108 // TODO(giolekva): take admin password as well
109}
110
111func (s *envServer) createEnv(c echo.Context) error {
112 var req createEnvReq
113 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
114 return err
115 }
116 keys, err := installer.NewSSHKeyPair()
117 if err != nil {
118 return err
119 }
120 {
121 readme := fmt.Sprintf("# %s PCloud environment", req.Name)
122 if err := s.ss.AddRepository(req.Name, readme); err != nil {
123 return err
124 }
125 fluxUserName := fmt.Sprintf("flux-%s", req.Name)
126 if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
127 return err
128 }
129 if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
130 return err
131 }
132 }
133 {
134 repo, err := s.ss.GetRepo(req.Name)
135 if repo == nil {
136 return err
137 }
138 if err := initNewEnv(installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
139 return err
140 }
141 }
142 {
143 repo, err := s.ss.GetRepo("pcloud")
144 if err != nil {
145 return err
146 }
147 ssPubKey, err := s.ss.GetPublicKey()
148 if err != nil {
149 return err
150 }
151 if err := addNewEnv(
152 installer.NewRepoIO(repo, s.ss.Signer),
153 req,
154 keys,
155 ssPubKey,
156 ); err != nil {
157 return err
158 }
159 }
160 return nil
161}
162
163func initNewEnv(r installer.RepoIO, req createEnvReq) error {
164 appManager, err := installer.NewAppManager(r)
165 if err != nil {
166 return err
167 }
168 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
169 // TODO(giolekva): env name and ip should come from pcloud repo config.yaml
170 // TODO(giolekva): private domain can be configurable as well
171 config := installer.Config{
172 Values: installer.Values{
173 PCloudEnvName: "pcloud",
174 Id: req.Name,
175 ContactEmail: req.ContactEmail,
176 Domain: req.Domain,
177 PrivateDomain: fmt.Sprintf("p.%s", req.Domain),
178 PublicIP: "46.49.35.44",
179 NamespacePrefix: fmt.Sprintf("%s-", req.Name),
180 },
181 }
182 if err := r.WriteYaml("config.yaml", config); err != nil {
183 return err
184 }
185 {
186 out, err := r.Writer("pcloud-charts.yaml")
187 if err != nil {
188 return err
189 }
190 defer out.Close()
191 _, err = out.Write([]byte(`
192apiVersion: source.toolkit.fluxcd.io/v1beta2
193kind: GitRepository
194metadata:
195 name: pcloud
196 namespace: lekva
197spec:
198 interval: 1m0s
199 url: https://github.com/giolekva/pcloud
200 ref:
201 branch: main
202`))
203 if err != nil {
204 return err
205 }
206 }
207 rootKust := installer.NewKustomization()
208 rootKust.AddResources("pcloud-charts.yaml", "apps")
209 if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
210 return err
211 }
212 appsKust := installer.NewKustomization()
213 if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
214 return err
215 }
216 r.CommitAndPush("initialize config")
217 {
218 app, err := appsRepo.Find("metallb-config-env")
219 if err != nil {
220 return err
221 }
222 if err := appManager.Install(*app, map[string]any{
223 "IngressPrivate": "10.1.0.1",
224 "Headscale": "10.1.0.2",
225 "SoftServe": "10.1.0.3",
226 "Rest": map[string]any{
227 "From": "10.1.0.100",
228 "To": "10.1.0.255",
229 },
230 }); err != nil {
231 return err
232 }
233 }
234 {
235 app, err := appsRepo.Find("ingress-private")
236 if err != nil {
237 return err
238 }
239 if err := appManager.Install(*app, map[string]any{
240 "GandiAPIToken": req.GandiAPIToken,
241 }); err != nil {
242 return err
243 }
244 }
245 {
246 app, err := appsRepo.Find("core-auth")
247 if err != nil {
248 return err
249 }
250 if err := appManager.Install(*app, map[string]any{
251 "Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
252 }); err != nil {
253 return err
254 }
255 }
256 {
257 app, err := appsRepo.Find("headscale")
258 if err != nil {
259 return err
260 }
261 if err := appManager.Install(*app, map[string]any{
262 "Subdomain": "headscale",
263 }); err != nil {
264 return err
265 }
266 }
267 {
268 app, err := appsRepo.Find("tailscale-proxy")
269 if err != nil {
270 return err
271 }
272 if err := appManager.Install(*app, map[string]any{
273 "Username": req.AdminUsername,
274 "IPSubnet": "10.1.0.0/24",
275 }); err != nil {
276 return err
277 }
278 // TODO(giolekva): headscale accept routes
279 }
280
281 return nil
282}
283
284func addNewEnv(
285 repoIO installer.RepoIO,
286 req createEnvReq,
287 keys installer.KeyPair,
288 pcloudRepoPublicKey []byte,
289) error {
290 kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
291 if err != nil {
292 return err
293 }
294 kust.AddResources(req.Name)
295 tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
296 if err != nil {
297 return err
298 }
299 for _, tmpl := range tmpls.Templates() {
300 dstPath := path.Join("environments", req.Name, tmpl.Name())
301 dst, err := repoIO.Writer(dstPath)
302 if err != nil {
303 return err
304 }
305 defer dst.Close()
306 if err := tmpl.Execute(dst, map[string]string{
307 "Name": req.Name,
308 "PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
309 "PublicKey": base64.StdEncoding.EncodeToString([]byte(keys.Public)),
310 "GitHost": envManagerFlags.repoIP,
311 "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", envManagerFlags.repoIP, pcloudRepoPublicKey))),
312 }); err != nil {
313 return err
314 }
315 }
316 if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
317 return err
318 }
319 if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
320 return err
321 }
322 return nil
323}