blob: 219c67cf13a59b144a6373d177d4c2ed06a56450 [file] [log] [blame]
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +04001package welcome
2
3import (
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +04004 "embed"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +04005 "encoding/json"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +04006 "errors"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +04007 "fmt"
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +04008 "html/template"
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +04009 "io"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040010 "io/fs"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040011 "log"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040012 "net"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040013 "net/http"
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +040014 "net/netip"
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +040015 "strings"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040016
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +040017 "github.com/gomarkdown/markdown"
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040018 "github.com/gorilla/mux"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040019
20 "github.com/giolekva/pcloud/core/installer"
21 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040022 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040023)
24
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040025//go:embed env-manager-tmpl/*
26var tmpls embed.FS
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040027
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040028var tmplsParsed templates
29
30func init() {
31 if t, err := parseTemplates(tmpls); err != nil {
32 panic(err)
33 } else {
34 tmplsParsed = t
35 }
36}
37
38type templates struct {
39 form *template.Template
40 status *template.Template
41}
42
43func parseTemplates(fs embed.FS) (templates, error) {
44 base, err := template.ParseFS(fs, "env-manager-tmpl/base.html")
45 if err != nil {
46 return templates{}, err
47 }
48 parse := func(path string) (*template.Template, error) {
49 if b, err := base.Clone(); err != nil {
50 return nil, err
51 } else {
52 return b.ParseFS(fs, path)
53 }
54 }
55 form, err := parse("env-manager-tmpl/form.html")
56 if err != nil {
57 return templates{}, err
58 }
59 status, err := parse("env-manager-tmpl/status.html")
60 if err != nil {
61 return templates{}, err
62 }
63 return templates{form, status}, nil
64}
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040065
66type Status string
67
68const (
69 StatusActive Status = "ACTIVE"
70 StatusAccepted Status = "ACCEPTED"
71)
72
73// TODO(giolekva): add CreatedAt and ValidUntil
74type invitation struct {
75 Token string `json:"token"`
76 Status Status `json:"status"`
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040077}
78
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040079type EnvServer struct {
80 port int
81 ss *soft.Client
82 repo installer.RepoIO
83 nsCreator installer.NamespaceCreator
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040084 dnsFetcher installer.ZoneStatusFetcher
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040085 nameGenerator installer.NameGenerator
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040086 tasks map[string]tasks.Task
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +040087 envInfo map[string]template.HTML
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040088 dns map[string]tasks.DNSZoneRef
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +040089 dnsPublished map[string]struct{}
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040090}
91
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040092func NewEnvServer(
93 port int,
94 ss *soft.Client,
95 repo installer.RepoIO,
96 nsCreator installer.NamespaceCreator,
97 dnsFetcher installer.ZoneStatusFetcher,
98 nameGenerator installer.NameGenerator,
99) *EnvServer {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400100 return &EnvServer{
101 port,
102 ss,
103 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400104 nsCreator,
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400105 dnsFetcher,
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400106 nameGenerator,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400107 make(map[string]tasks.Task),
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400108 make(map[string]template.HTML),
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400109 make(map[string]tasks.DNSZoneRef),
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400110 make(map[string]struct{}),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400111 }
112}
113
114func (s *EnvServer) Start() {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400115 r := mux.NewRouter()
116 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400117 r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400118 r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400119 r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
120 r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400121 r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400122 http.Handle("/", r)
123 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400124}
125
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400126func (s *EnvServer) monitorTask(w http.ResponseWriter, r *http.Request) {
127 vars := mux.Vars(r)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400128 key, ok := vars["key"]
129 if !ok {
130 http.Error(w, "Task key not provided", http.StatusBadRequest)
131 return
132 }
133 t, ok := s.tasks[key]
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400134 if !ok {
135 http.Error(w, "Task not found", http.StatusBadRequest)
136 return
137 }
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400138 dnsRecords := ""
139 if _, ok := s.dnsPublished[key]; !ok {
140 dnsRef, ok := s.dns[key]
141 if !ok {
142 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
143 return
144 }
145 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
146 // TODO(gio): check error type
147 if err != nil && (ready || len(info.Records) > 0) {
148 panic("!! SHOULD NOT REACH !!")
149 }
150 if !ready && len(info.Records) > 0 {
151 panic("!! SHOULD NOT REACH !!")
152 }
153 dnsRecords = info.Records
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400154 }
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400155 data := map[string]any{
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400156 "Root": t,
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400157 "EnvInfo": s.envInfo[key],
158 "DNSRecords": dnsRecords,
159 }
160 if err := tmplsParsed.status.Execute(w, data); err != nil {
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400161 http.Error(w, err.Error(), http.StatusInternalServerError)
162 return
163 }
164}
165
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400166func (s *EnvServer) publishDNSRecords(w http.ResponseWriter, r *http.Request) {
167 vars := mux.Vars(r)
168 key, ok := vars["key"]
169 if !ok {
170 http.Error(w, "Task key not provided", http.StatusBadRequest)
171 return
172 }
173 dnsRef, ok := s.dns[key]
174 if !ok {
175 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
176 return
177 }
178 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
179 // TODO(gio): check error type
180 if err != nil && (ready || len(info.Records) > 0) {
181 panic("!! SHOULD NOT REACH !!")
182 }
183 if !ready && len(info.Records) > 0 {
184 panic("!! SHOULD NOT REACH !!")
185 }
186 r.ParseForm()
187 if apiToken, err := getFormValue(r.PostForm, "api-token"); err != nil {
188 http.Error(w, err.Error(), http.StatusBadRequest)
189 return
190 } else {
191 p := NewGandiUpdater(apiToken)
192 zone := strings.Join(strings.Split(info.Zone, ".")[1:], ".") // TODO(gio): this is not gonna work with no subdomain case
193 if err := p.Update(zone, strings.Split(info.Records, "\n")); err != nil {
194 http.Error(w, err.Error(), http.StatusInternalServerError)
195 return
196 }
197 }
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400198 s.envInfo[key] = "Successfully published DNS records, waiting to propagate."
199 s.dnsPublished[key] = struct{}{}
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400200 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400201}
202
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400203func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400204 if err := tmplsParsed.form.Execute(w, nil); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400207}
208
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400209func (s *EnvServer) createInvitation(w http.ResponseWriter, r *http.Request) {
210 invitations, err := s.readInvitations()
211 if err != nil {
212 http.Error(w, err.Error(), http.StatusInternalServerError)
213 return
214 }
215 token, err := installer.NewFixedLengthRandomNameGenerator(100).Generate() // TODO(giolekva): use cryptographic tokens
216 if err != nil {
217 http.Error(w, err.Error(), http.StatusInternalServerError)
218 return
219
220 }
221 invitations = append(invitations, invitation{token, StatusActive})
222 if err := s.writeInvitations(invitations); err != nil {
223 http.Error(w, err.Error(), http.StatusInternalServerError)
224 return
225 }
226 if _, err := w.Write([]byte("OK")); err != nil {
227 http.Error(w, err.Error(), http.StatusInternalServerError)
228 return
229 }
230}
231
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400232type createEnvReq struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400233 Name string
234 ContactEmail string `json:"contactEmail"`
235 Domain string `json:"domain"`
236 AdminPublicKey string `json:"adminPublicKey"`
237 SecretToken string `json:"secretToken"`
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400238}
239
240func (s *EnvServer) readInvitations() ([]invitation, error) {
241 r, err := s.repo.Reader("invitations")
242 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400243 if errors.Is(err, fs.ErrNotExist) {
244 return make([]invitation, 0), nil
245 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400246 return nil, err
247 }
248 defer r.Close()
249 dec := json.NewDecoder(r)
250 invitations := make([]invitation, 0)
251 for {
252 var i invitation
253 if err := dec.Decode(&i); err == io.EOF {
254 break
255 }
256 invitations = append(invitations, i)
257 }
258 return invitations, nil
259}
260
261func (s *EnvServer) writeInvitations(invitations []invitation) error {
262 w, err := s.repo.Writer("invitations")
263 if err != nil {
264 return err
265 }
266 defer w.Close()
267 enc := json.NewEncoder(w)
268 for _, i := range invitations {
269 if err := enc.Encode(i); err != nil {
270 return err
271 }
272 }
273 return s.repo.CommitAndPush("Generated new invitation")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400274}
275
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400276func extractRequest(r *http.Request) (createEnvReq, error) {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400277 var req createEnvReq
278 if err := func() error {
279 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400280 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400281 return err
282 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400283 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400284 return err
285 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400286 if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400287 return err
288 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400289 if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400290 return err
291 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400292 if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
293 return err
294 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400295 return nil
296 }(); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400297 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400298 return createEnvReq{}, err
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400299 }
300 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400301 return req, nil
302}
303
304func (s *EnvServer) acceptInvitation(token string) error {
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400305 invitations, err := s.readInvitations()
306 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400307 return err
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400308 }
309 found := false
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400310 for i := range invitations {
311 if invitations[i].Token == token && invitations[i].Status == StatusActive {
312 invitations[i].Status = StatusAccepted
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400313 found = true
314 break
315 }
316 }
317 if !found {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400318 return fmt.Errorf("Invitation not found")
319 }
320 return s.writeInvitations(invitations)
321}
322
323func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
324 req, err := extractRequest(r)
325 if err != nil {
326 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400327 return
328 }
gio3cdee592024-04-17 10:15:56 +0400329 mgr, err := installer.NewInfraAppManager(s.repo, s.nsCreator)
330 if err != nil {
331 http.Error(w, err.Error(), http.StatusInternalServerError)
332 return
333 }
334 var infra installer.InfraConfig
335 if err := installer.ReadYaml(s.repo, "config.yaml", &infra); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400336 http.Error(w, err.Error(), http.StatusInternalServerError)
337 return
338 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400339 // if err := s.acceptInvitation(req.SecretToken); err != nil {
340 // http.Error(w, err.Error(), http.StatusInternalServerError)
341 // return
342 // }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400343 if name, err := s.nameGenerator.Generate(); err != nil {
344 http.Error(w, err.Error(), http.StatusInternalServerError)
345 return
346 } else {
347 req.Name = name
348 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400349 var cidrs installer.EnvCIDRs
gio3af43942024-04-16 08:13:50 +0400350 if err := installer.ReadYaml(s.repo, "env-cidrs.yaml", &cidrs); err != nil {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400351 http.Error(w, err.Error(), http.StatusInternalServerError)
352 return
353 }
354 startIP, err := findNextStartIP(cidrs)
355 if err != nil {
356 http.Error(w, err.Error(), http.StatusInternalServerError)
357 return
358 }
359 cidrs = append(cidrs, installer.EnvCIDR{req.Name, startIP})
gio3af43942024-04-16 08:13:50 +0400360 if err := installer.WriteYaml(s.repo, "env-cidrs.yaml", cidrs); err != nil {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400361 http.Error(w, err.Error(), http.StatusInternalServerError)
362 return
363 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400364 if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
365 http.Error(w, err.Error(), http.StatusInternalServerError)
366 return
367 }
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400368 key := func() string {
369 for {
370 key, err := s.nameGenerator.Generate()
371 if err == nil {
372 return key
373 }
374 }
375 }()
376 infoUpdater := func(info string) {
377 s.envInfo[key] = template.HTML(markdown.ToHTML([]byte(info), nil, nil))
378 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400379 t, dns := tasks.NewCreateEnvTask(
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400380 tasks.Env{
gio3cdee592024-04-17 10:15:56 +0400381 PCloudEnvName: infra.Name,
gio3af43942024-04-16 08:13:50 +0400382 Name: req.Name,
383 ContactEmail: req.ContactEmail,
384 Domain: req.Domain,
385 AdminPublicKey: req.AdminPublicKey,
386 NamespacePrefix: fmt.Sprintf("%s-", req.Name),
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400387 },
gio3cdee592024-04-17 10:15:56 +0400388 infra.PublicIP,
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400389 startIP,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400390 s.nsCreator,
391 s.repo,
gio3cdee592024-04-17 10:15:56 +0400392 mgr,
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +0400393 infoUpdater,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400394 )
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400395 s.tasks[key] = t
396 s.dns[key] = dns
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400397 go t.Start()
Giorgi Lekveishvilic85504d2023-12-20 19:29:47 +0400398 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400399}
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400400
401func findNextStartIP(cidrs installer.EnvCIDRs) (net.IP, error) {
402 m, err := netip.ParseAddr("10.0.0.0")
403 if err != nil {
404 return nil, err
405 }
406 for _, cidr := range cidrs {
407 i, err := netip.ParseAddr(cidr.IP.String())
408 if err != nil {
409 return nil, err
410 }
411 if i.Compare(m) > 0 {
412 m = i
413 }
414 }
415 sl := m.AsSlice()
416 sl[2]++
417 if sl[2] == 0b11111111 {
418 sl[2] = 0
419 sl[1]++
420 }
421 if sl[1] == 0b11111111 {
422 return nil, fmt.Errorf("Can not allocate")
423 }
424 ret, ok := netip.AddrFromSlice(sl)
425 if !ok {
426 return nil, fmt.Errorf("Must not reach")
427 }
428 return net.ParseIP(ret.String()), nil
429}