blob: f8ee70dee23a478f21c948d458bfcc50d9b4e0d6 [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 Lekveishvilia1e77902023-11-06 14:48:27 +040017 "github.com/gorilla/mux"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040018
19 "github.com/giolekva/pcloud/core/installer"
20 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040021 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040022)
23
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040024//go:embed env-manager-tmpl/*
25var tmpls embed.FS
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040026
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040027var tmplsParsed templates
28
29func init() {
30 if t, err := parseTemplates(tmpls); err != nil {
31 panic(err)
32 } else {
33 tmplsParsed = t
34 }
35}
36
37type templates struct {
38 form *template.Template
39 status *template.Template
40}
41
42func parseTemplates(fs embed.FS) (templates, error) {
43 base, err := template.ParseFS(fs, "env-manager-tmpl/base.html")
44 if err != nil {
45 return templates{}, err
46 }
47 parse := func(path string) (*template.Template, error) {
48 if b, err := base.Clone(); err != nil {
49 return nil, err
50 } else {
51 return b.ParseFS(fs, path)
52 }
53 }
54 form, err := parse("env-manager-tmpl/form.html")
55 if err != nil {
56 return templates{}, err
57 }
58 status, err := parse("env-manager-tmpl/status.html")
59 if err != nil {
60 return templates{}, err
61 }
62 return templates{form, status}, nil
63}
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040064
65type Status string
66
67const (
68 StatusActive Status = "ACTIVE"
69 StatusAccepted Status = "ACCEPTED"
70)
71
72// TODO(giolekva): add CreatedAt and ValidUntil
73type invitation struct {
74 Token string `json:"token"`
75 Status Status `json:"status"`
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040076}
77
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040078type EnvServer struct {
79 port int
80 ss *soft.Client
81 repo installer.RepoIO
82 nsCreator installer.NamespaceCreator
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040083 dnsFetcher installer.ZoneStatusFetcher
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040084 nameGenerator installer.NameGenerator
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040085 tasks map[string]tasks.Task
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040086 dns map[string]tasks.DNSZoneRef
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040087}
88
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040089func NewEnvServer(
90 port int,
91 ss *soft.Client,
92 repo installer.RepoIO,
93 nsCreator installer.NamespaceCreator,
94 dnsFetcher installer.ZoneStatusFetcher,
95 nameGenerator installer.NameGenerator,
96) *EnvServer {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040097 return &EnvServer{
98 port,
99 ss,
100 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400101 nsCreator,
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400102 dnsFetcher,
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400103 nameGenerator,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400104 make(map[string]tasks.Task),
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400105 make(map[string]tasks.DNSZoneRef),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400106 }
107}
108
109func (s *EnvServer) Start() {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400110 r := mux.NewRouter()
111 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400112 r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400113 r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400114 r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
115 r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400116 r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400117 http.Handle("/", r)
118 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400119}
120
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400121func (s *EnvServer) monitorTask(w http.ResponseWriter, r *http.Request) {
122 vars := mux.Vars(r)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400123 key, ok := vars["key"]
124 if !ok {
125 http.Error(w, "Task key not provided", http.StatusBadRequest)
126 return
127 }
128 t, ok := s.tasks[key]
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400129 if !ok {
130 http.Error(w, "Task not found", http.StatusBadRequest)
131 return
132 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400133 dnsRef, ok := s.dns[key]
134 if !ok {
135 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
136 return
137 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400138 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400139 // TODO(gio): check error type
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400140 if err != nil && (ready || len(info.Records) > 0) {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400141 panic("!! SHOULD NOT REACH !!")
142 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400143 if !ready && len(info.Records) > 0 {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400144 panic("!! SHOULD NOT REACH !!")
145 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400146 if err := tmplsParsed.status.Execute(w, map[string]any{
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400147 "Root": t,
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400148 "DNSRecords": info.Records,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400149 }); err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 return
152 }
153}
154
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400155func (s *EnvServer) publishDNSRecords(w http.ResponseWriter, r *http.Request) {
156 vars := mux.Vars(r)
157 key, ok := vars["key"]
158 if !ok {
159 http.Error(w, "Task key not provided", http.StatusBadRequest)
160 return
161 }
162 dnsRef, ok := s.dns[key]
163 if !ok {
164 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
165 return
166 }
167 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
168 // TODO(gio): check error type
169 if err != nil && (ready || len(info.Records) > 0) {
170 panic("!! SHOULD NOT REACH !!")
171 }
172 if !ready && len(info.Records) > 0 {
173 panic("!! SHOULD NOT REACH !!")
174 }
175 r.ParseForm()
176 if apiToken, err := getFormValue(r.PostForm, "api-token"); err != nil {
177 http.Error(w, err.Error(), http.StatusBadRequest)
178 return
179 } else {
180 p := NewGandiUpdater(apiToken)
181 zone := strings.Join(strings.Split(info.Zone, ".")[1:], ".") // TODO(gio): this is not gonna work with no subdomain case
182 if err := p.Update(zone, strings.Split(info.Records, "\n")); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 }
186 }
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400187 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400188}
189
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400190func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400191 if err := tmplsParsed.form.Execute(w, nil); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400192 http.Error(w, err.Error(), http.StatusInternalServerError)
193 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400194}
195
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400196func (s *EnvServer) createInvitation(w http.ResponseWriter, r *http.Request) {
197 invitations, err := s.readInvitations()
198 if err != nil {
199 http.Error(w, err.Error(), http.StatusInternalServerError)
200 return
201 }
202 token, err := installer.NewFixedLengthRandomNameGenerator(100).Generate() // TODO(giolekva): use cryptographic tokens
203 if err != nil {
204 http.Error(w, err.Error(), http.StatusInternalServerError)
205 return
206
207 }
208 invitations = append(invitations, invitation{token, StatusActive})
209 if err := s.writeInvitations(invitations); err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 }
213 if _, err := w.Write([]byte("OK")); err != nil {
214 http.Error(w, err.Error(), http.StatusInternalServerError)
215 return
216 }
217}
218
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400219type createEnvReq struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400220 Name string
221 ContactEmail string `json:"contactEmail"`
222 Domain string `json:"domain"`
223 AdminPublicKey string `json:"adminPublicKey"`
224 SecretToken string `json:"secretToken"`
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400225}
226
227func (s *EnvServer) readInvitations() ([]invitation, error) {
228 r, err := s.repo.Reader("invitations")
229 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400230 if errors.Is(err, fs.ErrNotExist) {
231 return make([]invitation, 0), nil
232 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400233 return nil, err
234 }
235 defer r.Close()
236 dec := json.NewDecoder(r)
237 invitations := make([]invitation, 0)
238 for {
239 var i invitation
240 if err := dec.Decode(&i); err == io.EOF {
241 break
242 }
243 invitations = append(invitations, i)
244 }
245 return invitations, nil
246}
247
248func (s *EnvServer) writeInvitations(invitations []invitation) error {
249 w, err := s.repo.Writer("invitations")
250 if err != nil {
251 return err
252 }
253 defer w.Close()
254 enc := json.NewEncoder(w)
255 for _, i := range invitations {
256 if err := enc.Encode(i); err != nil {
257 return err
258 }
259 }
260 return s.repo.CommitAndPush("Generated new invitation")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400261}
262
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400263func extractRequest(r *http.Request) (createEnvReq, error) {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400264 var req createEnvReq
265 if err := func() error {
266 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400267 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400268 return err
269 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400270 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400271 return err
272 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400273 if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400274 return err
275 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400276 if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400277 return err
278 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400279 if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
280 return err
281 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400282 return nil
283 }(); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400284 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400285 return createEnvReq{}, err
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400286 }
287 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400288 return req, nil
289}
290
291func (s *EnvServer) acceptInvitation(token string) error {
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400292 invitations, err := s.readInvitations()
293 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400294 return err
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400295 }
296 found := false
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400297 for i := range invitations {
298 if invitations[i].Token == token && invitations[i].Status == StatusActive {
299 invitations[i].Status = StatusAccepted
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400300 found = true
301 break
302 }
303 }
304 if !found {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400305 return fmt.Errorf("Invitation not found")
306 }
307 return s.writeInvitations(invitations)
308}
309
310func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
311 req, err := extractRequest(r)
312 if err != nil {
313 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400314 return
315 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400316 var env installer.EnvConfig
317 cr, err := s.repo.Reader("config.yaml")
318 if err != nil {
319 http.Error(w, err.Error(), http.StatusInternalServerError)
320 return
321 }
322 defer cr.Close()
323 if err := installer.ReadYaml(cr, &env); err != nil {
324 http.Error(w, err.Error(), http.StatusInternalServerError)
325 return
326 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400327 // if err := s.acceptInvitation(req.SecretToken); err != nil {
328 // http.Error(w, err.Error(), http.StatusInternalServerError)
329 // return
330 // }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400331 if name, err := s.nameGenerator.Generate(); err != nil {
332 http.Error(w, err.Error(), http.StatusInternalServerError)
333 return
334 } else {
335 req.Name = name
336 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400337 var cidrs installer.EnvCIDRs
338 cidrsR, err := s.repo.Reader("env-cidrs.yaml")
339 if err != nil {
340 http.Error(w, err.Error(), http.StatusInternalServerError)
341 return
342 }
343 defer cidrsR.Close()
344 if err := installer.ReadYaml(cidrsR, &cidrs); err != nil {
345 http.Error(w, err.Error(), http.StatusInternalServerError)
346 return
347 }
348 startIP, err := findNextStartIP(cidrs)
349 if err != nil {
350 http.Error(w, err.Error(), http.StatusInternalServerError)
351 return
352 }
353 cidrs = append(cidrs, installer.EnvCIDR{req.Name, startIP})
354 if err := s.repo.WriteYaml("env-cidrs.yaml", cidrs); err != nil {
355 http.Error(w, err.Error(), http.StatusInternalServerError)
356 return
357 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400358 if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
361 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400362 t, dns := tasks.NewCreateEnvTask(
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400363 tasks.Env{
364 PCloudEnvName: env.Name,
365 Name: req.Name,
366 ContactEmail: req.ContactEmail,
367 Domain: req.Domain,
368 AdminPublicKey: req.AdminPublicKey,
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400369 },
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400370 []net.IP{
371 net.ParseIP("135.181.48.180"),
372 net.ParseIP("65.108.39.172"),
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400373 },
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400374 startIP,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400375 s.nsCreator,
376 s.repo,
377 )
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400378 key := func() string {
379 for {
380 key, err := s.nameGenerator.Generate()
381 if err == nil {
382 return key
383 }
384 }
385 }()
386 s.tasks[key] = t
387 s.dns[key] = dns
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400388 go t.Start()
Giorgi Lekveishvilic85504d2023-12-20 19:29:47 +0400389 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400390}
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400391
392func findNextStartIP(cidrs installer.EnvCIDRs) (net.IP, error) {
393 m, err := netip.ParseAddr("10.0.0.0")
394 if err != nil {
395 return nil, err
396 }
397 for _, cidr := range cidrs {
398 i, err := netip.ParseAddr(cidr.IP.String())
399 if err != nil {
400 return nil, err
401 }
402 if i.Compare(m) > 0 {
403 m = i
404 }
405 }
406 sl := m.AsSlice()
407 sl[2]++
408 if sl[2] == 0b11111111 {
409 sl[2] = 0
410 sl[1]++
411 }
412 if sl[1] == 0b11111111 {
413 return nil, fmt.Errorf("Can not allocate")
414 }
415 ret, ok := netip.AddrFromSlice(sl)
416 if !ok {
417 return nil, fmt.Errorf("Must not reach")
418 }
419 return net.ParseIP(ret.String()), nil
420}