blob: 5436702bfdee9dd08e74ddac9c96b644de7de802 [file] [log] [blame]
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +04001package welcome
2
3import (
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +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 Lekveishvili081f18f2023-11-07 14:58:10 +04008 htemplate "html/template"
9 "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 Lekveishvilib4a9c982023-06-22 15:17:02 +040024//go:embed create-env.html
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040025var createEnvFormHtml []byte
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040026
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040027//go:embed env-created.html
28var envCreatedHtml string
29
30type Status string
31
32const (
33 StatusActive Status = "ACTIVE"
34 StatusAccepted Status = "ACCEPTED"
35)
36
37// TODO(giolekva): add CreatedAt and ValidUntil
38type invitation struct {
39 Token string `json:"token"`
40 Status Status `json:"status"`
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040041}
42
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040043type EnvServer struct {
44 port int
45 ss *soft.Client
46 repo installer.RepoIO
47 nsCreator installer.NamespaceCreator
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040048 dnsFetcher installer.ZoneStatusFetcher
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040049 nameGenerator installer.NameGenerator
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040050 tasks map[string]tasks.Task
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040051 dns map[string]tasks.DNSZoneRef
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040052}
53
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040054func NewEnvServer(
55 port int,
56 ss *soft.Client,
57 repo installer.RepoIO,
58 nsCreator installer.NamespaceCreator,
59 dnsFetcher installer.ZoneStatusFetcher,
60 nameGenerator installer.NameGenerator,
61) *EnvServer {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040062 return &EnvServer{
63 port,
64 ss,
65 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040066 nsCreator,
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040067 dnsFetcher,
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040068 nameGenerator,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040069 make(map[string]tasks.Task),
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040070 make(map[string]tasks.DNSZoneRef),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040071 }
72}
73
74func (s *EnvServer) Start() {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040075 r := mux.NewRouter()
76 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040077 r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +040078 r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040079 r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
80 r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040081 r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040082 http.Handle("/", r)
83 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040084}
85
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040086func (s *EnvServer) monitorTask(w http.ResponseWriter, r *http.Request) {
87 vars := mux.Vars(r)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040088 key, ok := vars["key"]
89 if !ok {
90 http.Error(w, "Task key not provided", http.StatusBadRequest)
91 return
92 }
93 t, ok := s.tasks[key]
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040094 if !ok {
95 http.Error(w, "Task not found", http.StatusBadRequest)
96 return
97 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040098 dnsRef, ok := s.dns[key]
99 if !ok {
100 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
101 return
102 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400103 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400104 // TODO(gio): check error type
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400105 if err != nil && (ready || len(info.Records) > 0) {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400106 panic("!! SHOULD NOT REACH !!")
107 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400108 if !ready && len(info.Records) > 0 {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400109 panic("!! SHOULD NOT REACH !!")
110 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400111 tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
112 if err != nil {
113 http.Error(w, err.Error(), http.StatusInternalServerError)
114 return
115 }
116 if err := tmpl.Execute(w, map[string]any{
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400117 "Root": t,
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400118 "DNSRecords": info.Records,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400119 }); err != nil {
120 http.Error(w, err.Error(), http.StatusInternalServerError)
121 return
122 }
123}
124
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400125func (s *EnvServer) publishDNSRecords(w http.ResponseWriter, r *http.Request) {
126 vars := mux.Vars(r)
127 key, ok := vars["key"]
128 if !ok {
129 http.Error(w, "Task key not provided", http.StatusBadRequest)
130 return
131 }
132 dnsRef, ok := s.dns[key]
133 if !ok {
134 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
135 return
136 }
137 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
138 // TODO(gio): check error type
139 if err != nil && (ready || len(info.Records) > 0) {
140 panic("!! SHOULD NOT REACH !!")
141 }
142 if !ready && len(info.Records) > 0 {
143 panic("!! SHOULD NOT REACH !!")
144 }
145 r.ParseForm()
146 if apiToken, err := getFormValue(r.PostForm, "api-token"); err != nil {
147 http.Error(w, err.Error(), http.StatusBadRequest)
148 return
149 } else {
150 p := NewGandiUpdater(apiToken)
151 zone := strings.Join(strings.Split(info.Zone, ".")[1:], ".") // TODO(gio): this is not gonna work with no subdomain case
152 if err := p.Update(zone, strings.Split(info.Records, "\n")); err != nil {
153 http.Error(w, err.Error(), http.StatusInternalServerError)
154 return
155 }
156 }
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400157 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400158}
159
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400160func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400161 if _, err := w.Write(createEnvFormHtml); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400162 http.Error(w, err.Error(), http.StatusInternalServerError)
163 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400164}
165
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400166func (s *EnvServer) createInvitation(w http.ResponseWriter, r *http.Request) {
167 invitations, err := s.readInvitations()
168 if err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
170 return
171 }
172 token, err := installer.NewFixedLengthRandomNameGenerator(100).Generate() // TODO(giolekva): use cryptographic tokens
173 if err != nil {
174 http.Error(w, err.Error(), http.StatusInternalServerError)
175 return
176
177 }
178 invitations = append(invitations, invitation{token, StatusActive})
179 if err := s.writeInvitations(invitations); err != nil {
180 http.Error(w, err.Error(), http.StatusInternalServerError)
181 return
182 }
183 if _, err := w.Write([]byte("OK")); err != nil {
184 http.Error(w, err.Error(), http.StatusInternalServerError)
185 return
186 }
187}
188
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400189type createEnvReq struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400190 Name string
191 ContactEmail string `json:"contactEmail"`
192 Domain string `json:"domain"`
193 AdminPublicKey string `json:"adminPublicKey"`
194 SecretToken string `json:"secretToken"`
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400195}
196
197func (s *EnvServer) readInvitations() ([]invitation, error) {
198 r, err := s.repo.Reader("invitations")
199 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400200 if errors.Is(err, fs.ErrNotExist) {
201 return make([]invitation, 0), nil
202 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400203 return nil, err
204 }
205 defer r.Close()
206 dec := json.NewDecoder(r)
207 invitations := make([]invitation, 0)
208 for {
209 var i invitation
210 if err := dec.Decode(&i); err == io.EOF {
211 break
212 }
213 invitations = append(invitations, i)
214 }
215 return invitations, nil
216}
217
218func (s *EnvServer) writeInvitations(invitations []invitation) error {
219 w, err := s.repo.Writer("invitations")
220 if err != nil {
221 return err
222 }
223 defer w.Close()
224 enc := json.NewEncoder(w)
225 for _, i := range invitations {
226 if err := enc.Encode(i); err != nil {
227 return err
228 }
229 }
230 return s.repo.CommitAndPush("Generated new invitation")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400231}
232
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400233func extractRequest(r *http.Request) (createEnvReq, error) {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400234 var req createEnvReq
235 if err := func() error {
236 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400237 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400238 return err
239 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400240 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400241 return err
242 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400243 if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400244 return err
245 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400246 if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400247 return err
248 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400249 if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
250 return err
251 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400252 return nil
253 }(); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400254 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400255 return createEnvReq{}, err
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400256 }
257 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400258 return req, nil
259}
260
261func (s *EnvServer) acceptInvitation(token string) error {
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400262 invitations, err := s.readInvitations()
263 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400264 return err
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400265 }
266 found := false
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400267 for i := range invitations {
268 if invitations[i].Token == token && invitations[i].Status == StatusActive {
269 invitations[i].Status = StatusAccepted
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400270 found = true
271 break
272 }
273 }
274 if !found {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400275 return fmt.Errorf("Invitation not found")
276 }
277 return s.writeInvitations(invitations)
278}
279
280func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
281 req, err := extractRequest(r)
282 if err != nil {
283 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400284 return
285 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400286 var env installer.EnvConfig
287 cr, err := s.repo.Reader("config.yaml")
288 if err != nil {
289 http.Error(w, err.Error(), http.StatusInternalServerError)
290 return
291 }
292 defer cr.Close()
293 if err := installer.ReadYaml(cr, &env); err != nil {
294 http.Error(w, err.Error(), http.StatusInternalServerError)
295 return
296 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400297 if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
298 http.Error(w, err.Error(), http.StatusInternalServerError)
299 return
300 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400301 // if err := s.acceptInvitation(req.SecretToken); err != nil {
302 // http.Error(w, err.Error(), http.StatusInternalServerError)
303 // return
304 // }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400305 if name, err := s.nameGenerator.Generate(); err != nil {
306 http.Error(w, err.Error(), http.StatusInternalServerError)
307 return
308 } else {
309 req.Name = name
310 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400311 var cidrs installer.EnvCIDRs
312 cidrsR, err := s.repo.Reader("env-cidrs.yaml")
313 if err != nil {
314 http.Error(w, err.Error(), http.StatusInternalServerError)
315 return
316 }
317 defer cidrsR.Close()
318 if err := installer.ReadYaml(cidrsR, &cidrs); err != nil {
319 http.Error(w, err.Error(), http.StatusInternalServerError)
320 return
321 }
322 startIP, err := findNextStartIP(cidrs)
323 if err != nil {
324 http.Error(w, err.Error(), http.StatusInternalServerError)
325 return
326 }
327 cidrs = append(cidrs, installer.EnvCIDR{req.Name, startIP})
328 if err := s.repo.WriteYaml("env-cidrs.yaml", cidrs); err != nil {
329 http.Error(w, err.Error(), http.StatusInternalServerError)
330 return
331 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400332 t, dns := tasks.NewCreateEnvTask(
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400333 tasks.Env{
334 PCloudEnvName: env.Name,
335 Name: req.Name,
336 ContactEmail: req.ContactEmail,
337 Domain: req.Domain,
338 AdminPublicKey: req.AdminPublicKey,
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400339 },
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400340 []net.IP{
341 net.ParseIP("135.181.48.180"),
342 net.ParseIP("65.108.39.172"),
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400343 },
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400344 startIP,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400345 s.nsCreator,
346 s.repo,
347 )
Giorgi Lekveishvili1eec3e12023-12-18 21:12:29 +0400348 key := func() string {
349 for {
350 key, err := s.nameGenerator.Generate()
351 if err == nil {
352 return key
353 }
354 }
355 }()
356 s.tasks[key] = t
357 s.dns[key] = dns
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400358 go t.Start()
Giorgi Lekveishvilic85504d2023-12-20 19:29:47 +0400359 http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400360}
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400361
362func findNextStartIP(cidrs installer.EnvCIDRs) (net.IP, error) {
363 m, err := netip.ParseAddr("10.0.0.0")
364 if err != nil {
365 return nil, err
366 }
367 for _, cidr := range cidrs {
368 i, err := netip.ParseAddr(cidr.IP.String())
369 if err != nil {
370 return nil, err
371 }
372 if i.Compare(m) > 0 {
373 m = i
374 }
375 }
376 sl := m.AsSlice()
377 sl[2]++
378 if sl[2] == 0b11111111 {
379 sl[2] = 0
380 sl[1]++
381 }
382 if sl[1] == 0b11111111 {
383 return nil, fmt.Errorf("Can not allocate")
384 }
385 ret, ok := netip.AddrFromSlice(sl)
386 if !ok {
387 return nil, fmt.Errorf("Must not reach")
388 }
389 return net.ParseIP(ret.String()), nil
390}