blob: 20b43546616e9d293b73e4de776e23bbc9d6c81f [file] [log] [blame]
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +04001package welcome
2
3import (
4 "embed"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "log"
9 "net/http"
10 "path"
11 "text/template"
12
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040013 "github.com/charmbracelet/keygen"
14 "github.com/gorilla/mux"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040015
16 "github.com/giolekva/pcloud/core/installer"
17 "github.com/giolekva/pcloud/core/installer/soft"
18)
19
20//go:embed env-tmpl
21var filesTmpls embed.FS
22
23//go:embed create-env.html
24var createEnvFormHtml string
25
26type EnvServer struct {
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040027 port int
28 ss *soft.Client
29 repo installer.RepoIO
30 nsCreator installer.NamespaceCreator
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040031}
32
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040033func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO, nsCreator installer.NamespaceCreator) *EnvServer {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040034 return &EnvServer{
35 port,
36 ss,
37 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040038 nsCreator,
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040039 }
40}
41
42func (s *EnvServer) Start() {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040043 r := mux.NewRouter()
44 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
45 r.Path("/env").Methods("GET").HandlerFunc(s.createEnvForm)
46 r.Path("/env").Methods("POST").HandlerFunc(s.createEnv)
47 http.Handle("/", r)
48 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040049}
50
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040051func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
52 log.Printf("asdasd\n")
53 if _, err := w.Write([]byte(createEnvFormHtml)); err != nil {
54 http.Error(w, err.Error(), http.StatusInternalServerError)
55 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040056}
57
58type createEnvReq struct {
59 Name string `json:"name"`
60 ContactEmail string `json:"contactEmail"`
61 Domain string `json:"domain"`
62}
63
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040064func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040065 var req createEnvReq
66 if err := func() error {
67 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040068 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040069 return err
70 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040071 if req.Name, err = getFormValue(r.PostForm, "name"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040072 return err
73 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040074 if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040075 return err
76 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040077 if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040078 return err
79 }
80 return nil
81 }(); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040082 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
83 http.Error(w, err.Error(), http.StatusInternalServerError)
84 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040085 }
86 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040087 fluxUserName := fmt.Sprintf("flux-%s", req.Name)
88 keys, err := installer.NewSSHKeyPair(fluxUserName)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040089 if err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040090 http.Error(w, err.Error(), http.StatusInternalServerError)
91 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040092 }
93 {
94 readme := fmt.Sprintf("# %s PCloud environment", req.Name)
95 if err := s.ss.AddRepository(req.Name, readme); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040096 http.Error(w, err.Error(), http.StatusInternalServerError)
97 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040098 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040099 if err := s.ss.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
100 http.Error(w, err.Error(), http.StatusInternalServerError)
101 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400102 }
103 if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400104 http.Error(w, err.Error(), http.StatusInternalServerError)
105 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400106 }
107 }
108 {
109 repo, err := s.ss.GetRepo(req.Name)
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400110 if err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400111 http.Error(w, err.Error(), http.StatusInternalServerError)
112 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400113 }
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400114 var env installer.EnvConfig
115 r, err := s.repo.Reader("config.yaml")
116 if err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400117 http.Error(w, err.Error(), http.StatusInternalServerError)
118 return
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400119 }
120 defer r.Close()
121 if err := installer.ReadYaml(r, &env); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400122 http.Error(w, err.Error(), http.StatusInternalServerError)
123 return
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400124 }
125 if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), s.nsCreator, req, env); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400126 http.Error(w, err.Error(), http.StatusInternalServerError)
127 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400128 }
129 }
130 {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400131 ssPubKey, err := s.ss.GetPublicKey()
132 if err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400133 http.Error(w, err.Error(), http.StatusInternalServerError)
134 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400135 }
136 if err := addNewEnv(
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400137 s.repo,
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400138 req,
139 keys,
140 ssPubKey,
141 ); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400142 http.Error(w, err.Error(), http.StatusInternalServerError)
143 return
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400144 }
145 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400146 if _, err := w.Write([]byte("OK")); err != nil {
147 http.Error(w, err.Error(), http.StatusInternalServerError)
148 return
149 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400150}
151
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400152func initNewEnv(
153 ss *soft.Client,
154 r installer.RepoIO,
155 nsCreator installer.NamespaceCreator,
156 req createEnvReq,
157 env installer.EnvConfig,
158) error {
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400159 appManager, err := installer.NewAppManager(r, nsCreator)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400160 if err != nil {
161 return err
162 }
163 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400164 // TODO(giolekva): private domain can be configurable as well
165 config := installer.Config{
166 Values: installer.Values{
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400167 PCloudEnvName: env.Name,
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400168 Id: req.Name,
169 ContactEmail: req.ContactEmail,
170 Domain: req.Domain,
171 PrivateDomain: fmt.Sprintf("p.%s", req.Domain),
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400172 PublicIP: env.PublicIP,
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400173 NamespacePrefix: fmt.Sprintf("%s-", req.Name),
174 },
175 }
176 if err := r.WriteYaml("config.yaml", config); err != nil {
177 return err
178 }
179 {
180 out, err := r.Writer("pcloud-charts.yaml")
181 if err != nil {
182 return err
183 }
184 defer out.Close()
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400185 _, err = out.Write([]byte(fmt.Sprintf(`
Giorgi Lekveishvilibf1e6e82023-07-12 11:57:24 +0400186apiVersion: source.toolkit.fluxcd.io/v1
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400187kind: GitRepository
188metadata:
189 name: pcloud
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400190 namespace: %s
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400191spec:
192 interval: 1m0s
193 url: https://github.com/giolekva/pcloud
194 ref:
195 branch: main
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400196`, req.Name)))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400197 if err != nil {
198 return err
199 }
200 }
201 rootKust := installer.NewKustomization()
202 rootKust.AddResources("pcloud-charts.yaml", "apps")
203 if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
204 return err
205 }
206 appsKust := installer.NewKustomization()
207 if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
208 return err
209 }
210 r.CommitAndPush("initialize config")
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400211 nsGen := installer.NewPrefixGenerator(req.Name + "-")
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400212 emptySuffixGen := installer.NewEmptySuffixGenerator()
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400213 {
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400214 app, err := appsRepo.Find("metallb-ipaddresspool")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400215 if err != nil {
216 return err
217 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400218 if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400219 "Name": fmt.Sprintf("%s-ingress-private", req.Name),
220 "From": "10.1.0.1",
221 "To": "10.1.0.1",
222 "AutoAssign": false,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400223 "Namespace": "metallb-system",
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400224 }); err != nil {
225 return err
226 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400227 if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400228 "Name": fmt.Sprintf("%s-headscale", req.Name),
229 "From": "10.1.0.2",
230 "To": "10.1.0.2",
231 "AutoAssign": false,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400232 "Namespace": "metallb-system",
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400233 }); err != nil {
234 return err
235 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400236 if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-soft-serve"), map[string]any{
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400237 "Name": fmt.Sprintf("%s-soft-serve", req.Name), // TODO(giolekva): rename to config repo
238 "From": "10.1.0.3",
239 "To": "10.1.0.3",
240 "AutoAssign": false,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400241 "Namespace": "metallb-system",
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400242 }); err != nil {
243 return err
244 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400245 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
Giorgi Lekveishvili41563dc2023-07-20 10:37:35 +0400246 "Name": req.Name,
247 "From": "10.1.0.100",
248 "To": "10.1.0.254",
249 "AutoAssign": false,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400250 "Namespace": "metallb-system",
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400251 }); err != nil {
252 return err
253 }
254 }
255 {
256 app, err := appsRepo.Find("ingress-private")
257 if err != nil {
258 return err
259 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400260 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{}); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400261 return err
262 }
263 }
264 {
265 app, err := appsRepo.Find("certificate-issuer-public")
266 if err != nil {
267 return err
268 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400269 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{}); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400270 return err
271 }
272 }
273 {
274 app, err := appsRepo.Find("core-auth")
275 if err != nil {
276 return err
277 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400278 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400279 "Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
280 }); err != nil {
281 return err
282 }
283 }
284 {
285 app, err := appsRepo.Find("headscale")
286 if err != nil {
287 return err
288 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400289 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400290 "Subdomain": "headscale",
291 }); err != nil {
292 return err
293 }
294 }
295 {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400296 keys, err := installer.NewSSHKeyPair("welcome")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400297 if err != nil {
298 return err
299 }
300 user := fmt.Sprintf("%s-welcome", req.Name)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400301 if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400302 return err
303 }
304 if err := ss.AddCollaborator(req.Name, user); err != nil {
305 return err
306 }
307 app, err := appsRepo.Find("welcome")
308 if err != nil {
309 return err
310 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400311 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400312 "RepoAddr": ss.GetRepoAddress(req.Name),
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400313 "SSHPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400314 }); err != nil {
315 return err
316 }
317 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318 {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400319 user := fmt.Sprintf("%s-appmanager", req.Name)
320 keys, err := installer.NewSSHKeyPair(user)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400321 if err != nil {
322 return err
323 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400324 if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400325 return err
326 }
327 if err := ss.AddCollaborator(req.Name, user); err != nil {
328 return err
329 }
330 app, err := appsRepo.Find("app-manager") // TODO(giolekva): configure
331 if err != nil {
332 return err
333 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400334 if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400335 "RepoAddr": ss.GetRepoAddress(req.Name),
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400336 "SSHPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400337 }); err != nil {
338 return err
339 }
340 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400341 return nil
342}
343
344func addNewEnv(
345 repoIO installer.RepoIO,
346 req createEnvReq,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400347 keys *keygen.KeyPair,
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400348 pcloudRepoPublicKey []byte,
349) error {
350 kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
351 if err != nil {
352 return err
353 }
354 kust.AddResources(req.Name)
355 tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
356 if err != nil {
357 return err
358 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400359 repoIP := repoIO.Addr().Addr().String()
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400360 for _, tmpl := range tmpls.Templates() {
361 dstPath := path.Join("environments", req.Name, tmpl.Name())
362 dst, err := repoIO.Writer(dstPath)
363 if err != nil {
364 return err
365 }
366 defer dst.Close()
367 if err := tmpl.Execute(dst, map[string]string{
368 "Name": req.Name,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400369 "PrivateKey": base64.StdEncoding.EncodeToString(keys.RawPrivateKey()),
370 "PublicKey": base64.StdEncoding.EncodeToString(keys.RawAuthorizedKey()),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400371 "GitHost": repoIP,
372 "KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoIP, pcloudRepoPublicKey))),
373 }); err != nil {
374 return err
375 }
376 }
377 if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
378 return err
379 }
380 if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
381 return err
382 }
383 return nil
384}