blob: 7514adfa3b670b4198c2936cee28c9124f82a448 [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 Lekveishvili1caed362023-12-13 16:29:43 +040014 "strings"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040015
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040016 "github.com/gorilla/mux"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040017
18 "github.com/giolekva/pcloud/core/installer"
19 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040020 "github.com/giolekva/pcloud/core/installer/tasks"
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040021)
22
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040023//go:embed create-env.html
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040024var createEnvFormHtml []byte
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040025
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040026//go:embed env-created.html
27var envCreatedHtml string
28
29type Status string
30
31const (
32 StatusActive Status = "ACTIVE"
33 StatusAccepted Status = "ACCEPTED"
34)
35
36// TODO(giolekva): add CreatedAt and ValidUntil
37type invitation struct {
38 Token string `json:"token"`
39 Status Status `json:"status"`
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040040}
41
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040042type EnvServer struct {
43 port int
44 ss *soft.Client
45 repo installer.RepoIO
46 nsCreator installer.NamespaceCreator
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040047 dnsFetcher installer.ZoneStatusFetcher
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040048 nameGenerator installer.NameGenerator
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040049 tasks map[string]tasks.Task
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040050 dns map[string]tasks.DNSZoneRef
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040051}
52
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040053func NewEnvServer(
54 port int,
55 ss *soft.Client,
56 repo installer.RepoIO,
57 nsCreator installer.NamespaceCreator,
58 dnsFetcher installer.ZoneStatusFetcher,
59 nameGenerator installer.NameGenerator,
60) *EnvServer {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040061 return &EnvServer{
62 port,
63 ss,
64 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040065 nsCreator,
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040066 dnsFetcher,
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040067 nameGenerator,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040068 make(map[string]tasks.Task),
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040069 make(map[string]tasks.DNSZoneRef),
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040070 }
71}
72
73func (s *EnvServer) Start() {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040074 r := mux.NewRouter()
75 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040076 r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +040077 r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040078 r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
79 r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +040080 r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040081 http.Handle("/", r)
82 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +040083}
84
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040085func (s *EnvServer) monitorTask(w http.ResponseWriter, r *http.Request) {
86 vars := mux.Vars(r)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040087 key, ok := vars["key"]
88 if !ok {
89 http.Error(w, "Task key not provided", http.StatusBadRequest)
90 return
91 }
92 t, ok := s.tasks[key]
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040093 if !ok {
94 http.Error(w, "Task not found", http.StatusBadRequest)
95 return
96 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +040097 dnsRef, ok := s.dns[key]
98 if !ok {
99 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
100 return
101 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400102 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400103 // TODO(gio): check error type
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400104 if err != nil && (ready || len(info.Records) > 0) {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400105 panic("!! SHOULD NOT REACH !!")
106 }
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400107 if !ready && len(info.Records) > 0 {
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400108 panic("!! SHOULD NOT REACH !!")
109 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400110 tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
111 if err != nil {
112 http.Error(w, err.Error(), http.StatusInternalServerError)
113 return
114 }
115 if err := tmpl.Execute(w, map[string]any{
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400116 "Root": t,
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400117 "DNSRecords": info.Records,
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400118 }); err != nil {
119 http.Error(w, err.Error(), http.StatusInternalServerError)
120 return
121 }
122}
123
Giorgi Lekveishvili1caed362023-12-13 16:29:43 +0400124func (s *EnvServer) publishDNSRecords(w http.ResponseWriter, r *http.Request) {
125 vars := mux.Vars(r)
126 key, ok := vars["key"]
127 if !ok {
128 http.Error(w, "Task key not provided", http.StatusBadRequest)
129 return
130 }
131 dnsRef, ok := s.dns[key]
132 if !ok {
133 http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
134 return
135 }
136 err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
137 // TODO(gio): check error type
138 if err != nil && (ready || len(info.Records) > 0) {
139 panic("!! SHOULD NOT REACH !!")
140 }
141 if !ready && len(info.Records) > 0 {
142 panic("!! SHOULD NOT REACH !!")
143 }
144 r.ParseForm()
145 if apiToken, err := getFormValue(r.PostForm, "api-token"); err != nil {
146 http.Error(w, err.Error(), http.StatusBadRequest)
147 return
148 } else {
149 p := NewGandiUpdater(apiToken)
150 zone := strings.Join(strings.Split(info.Zone, ".")[1:], ".") // TODO(gio): this is not gonna work with no subdomain case
151 if err := p.Update(zone, strings.Split(info.Records, "\n")); err != nil {
152 http.Error(w, err.Error(), http.StatusInternalServerError)
153 return
154 }
155 }
156 http.Redirect(w, r, "/env/foo", http.StatusSeeOther)
157}
158
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400159func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400160 if _, err := w.Write(createEnvFormHtml); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400161 http.Error(w, err.Error(), http.StatusInternalServerError)
162 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400163}
164
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400165func (s *EnvServer) createInvitation(w http.ResponseWriter, r *http.Request) {
166 invitations, err := s.readInvitations()
167 if err != nil {
168 http.Error(w, err.Error(), http.StatusInternalServerError)
169 return
170 }
171 token, err := installer.NewFixedLengthRandomNameGenerator(100).Generate() // TODO(giolekva): use cryptographic tokens
172 if err != nil {
173 http.Error(w, err.Error(), http.StatusInternalServerError)
174 return
175
176 }
177 invitations = append(invitations, invitation{token, StatusActive})
178 if err := s.writeInvitations(invitations); err != nil {
179 http.Error(w, err.Error(), http.StatusInternalServerError)
180 return
181 }
182 if _, err := w.Write([]byte("OK")); err != nil {
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return
185 }
186}
187
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400188type createEnvReq struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400189 Name string
190 ContactEmail string `json:"contactEmail"`
191 Domain string `json:"domain"`
192 AdminPublicKey string `json:"adminPublicKey"`
193 SecretToken string `json:"secretToken"`
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400194}
195
196func (s *EnvServer) readInvitations() ([]invitation, error) {
197 r, err := s.repo.Reader("invitations")
198 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400199 if errors.Is(err, fs.ErrNotExist) {
200 return make([]invitation, 0), nil
201 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400202 return nil, err
203 }
204 defer r.Close()
205 dec := json.NewDecoder(r)
206 invitations := make([]invitation, 0)
207 for {
208 var i invitation
209 if err := dec.Decode(&i); err == io.EOF {
210 break
211 }
212 invitations = append(invitations, i)
213 }
214 return invitations, nil
215}
216
217func (s *EnvServer) writeInvitations(invitations []invitation) error {
218 w, err := s.repo.Writer("invitations")
219 if err != nil {
220 return err
221 }
222 defer w.Close()
223 enc := json.NewEncoder(w)
224 for _, i := range invitations {
225 if err := enc.Encode(i); err != nil {
226 return err
227 }
228 }
229 return s.repo.CommitAndPush("Generated new invitation")
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400230}
231
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400232func extractRequest(r *http.Request) (createEnvReq, error) {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400233 var req createEnvReq
234 if err := func() error {
235 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400236 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400237 return err
238 }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400239 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400240 return err
241 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400242 if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400243 return err
244 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400245 if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400246 return err
247 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400248 if req.AdminPublicKey, err = getFormValue(r.PostForm, "admin-public-key"); err != nil {
249 return err
250 }
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400251 return nil
252 }(); err != nil {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400253 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400254 return createEnvReq{}, err
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400255 }
256 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400257 return req, nil
258}
259
260func (s *EnvServer) acceptInvitation(token string) error {
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400261 invitations, err := s.readInvitations()
262 if err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400263 return err
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400264 }
265 found := false
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400266 for i := range invitations {
267 if invitations[i].Token == token && invitations[i].Status == StatusActive {
268 invitations[i].Status = StatusAccepted
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400269 found = true
270 break
271 }
272 }
273 if !found {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400274 return fmt.Errorf("Invitation not found")
275 }
276 return s.writeInvitations(invitations)
277}
278
279func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
280 req, err := extractRequest(r)
281 if err != nil {
282 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400283 return
284 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400285 var env installer.EnvConfig
286 cr, err := s.repo.Reader("config.yaml")
287 if err != nil {
288 http.Error(w, err.Error(), http.StatusInternalServerError)
289 return
290 }
291 defer cr.Close()
292 if err := installer.ReadYaml(cr, &env); err != nil {
293 http.Error(w, err.Error(), http.StatusInternalServerError)
294 return
295 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400296 // if err := s.acceptInvitation(req.SecretToken); err != nil {
297 // http.Error(w, err.Error(), http.StatusInternalServerError)
298 // return
299 // }
Giorgi Lekveishvili081f18f2023-11-07 14:58:10 +0400300 if name, err := s.nameGenerator.Generate(); err != nil {
301 http.Error(w, err.Error(), http.StatusInternalServerError)
302 return
303 } else {
304 req.Name = name
305 }
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400306 t, dns := tasks.NewCreateEnvTask(
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400307 tasks.Env{
308 PCloudEnvName: env.Name,
309 Name: req.Name,
310 ContactEmail: req.ContactEmail,
311 Domain: req.Domain,
312 AdminPublicKey: req.AdminPublicKey,
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400313 },
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400314 []net.IP{
315 net.ParseIP("135.181.48.180"),
316 net.ParseIP("65.108.39.172"),
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400317 },
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400318 s.nsCreator,
319 s.repo,
320 )
321 s.tasks["foo"] = t
Giorgi Lekveishvilicd9e42c2023-12-13 09:49:44 +0400322 s.dns["foo"] = dns
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400323 go t.Start()
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400324 http.Redirect(w, r, "/env/foo", http.StatusSeeOther)
Giorgi Lekveishvilib4a9c982023-06-22 15:17:02 +0400325}