blob: 6c7d361e2583a285100fcc3a615c5545a3dd58d5 [file] [log] [blame]
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04001package main
2
3import (
gioefa0ed42024-06-13 12:31:43 +04004 "crypto/rand"
giob1c4e542024-07-15 12:10:52 +04005 "encoding/base64"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04006 "encoding/json"
7 "flag"
8 "fmt"
9 "io"
10 "log"
gioefa0ed42024-06-13 12:31:43 +040011 "math/big"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040012 "net/http"
13 "os"
14 "strconv"
15 "strings"
gioefa0ed42024-06-13 12:31:43 +040016 "sync"
17 "time"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040018
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040019 "github.com/giolekva/pcloud/core/installer/soft"
20
21 "golang.org/x/crypto/ssh"
22)
23
Davit Tabidze6bf29832024-06-17 16:51:54 +040024const (
25 secretLength = 20
26)
27
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040028var port = flag.Int("port", 8080, "Port to listen on")
29var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
30var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
31var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
32
33type client interface {
34 ReadRelease() (map[string]any, error)
35 WriteRelease(rel map[string]any, meta string) error
36}
37
38type repoClient struct {
gioe72b54f2024-04-22 10:44:41 +040039 repo soft.RepoIO
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040040 path string
41}
42
43func (c *repoClient) ReadRelease() (map[string]any, error) {
44 if err := c.repo.Pull(); err != nil {
45 return nil, err
46 }
gioff2a29a2024-05-01 17:06:42 +040047 ingressRel := map[string]any{}
48 if err := soft.ReadYaml(c.repo, c.path, &ingressRel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040049 return nil, err
50 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040051 return ingressRel, nil
52}
53
54func (c *repoClient) WriteRelease(rel map[string]any, meta string) error {
gioff2a29a2024-05-01 17:06:42 +040055 if err := soft.WriteYaml(c.repo, c.path, rel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040056 return err
57 }
58 return c.repo.CommitAndPush(meta)
59}
60
61type server struct {
gioefa0ed42024-06-13 12:31:43 +040062 l sync.Locker
63 s *http.Server
64 r *http.ServeMux
65 client client
66 reserve map[int]string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040067}
68
69func newServer(port int, client client) *server {
70 r := http.NewServeMux()
71 s := &http.Server{
72 Addr: fmt.Sprintf(":%d", port),
73 Handler: r,
74 }
gioefa0ed42024-06-13 12:31:43 +040075 return &server{&sync.Mutex{}, s, r, client, make(map[int]string)}
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040076}
77
78func (s *server) Start() error {
gioefa0ed42024-06-13 12:31:43 +040079 s.r.HandleFunc("/api/reserve", s.handleReserve)
giocdfa3722024-06-13 20:10:14 +040080 s.r.HandleFunc("/api/allocate", s.handleAllocate)
81 s.r.HandleFunc("/api/remove", s.handleRemove)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040082 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
83 return err
84 }
85 return nil
86}
87
88func (s *server) Close() error {
89 return s.s.Close()
90}
91
92type allocateReq struct {
93 Protocol string `json:"protocol"`
94 SourcePort int `json:"sourcePort"`
95 TargetService string `json:"targetService"`
96 TargetPort int `json:"targetPort"`
giobd7ab0b2024-06-17 12:55:17 +040097 Secret string `json:"secret"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040098}
99
giocdfa3722024-06-13 20:10:14 +0400100type removeReq struct {
101 Protocol string `json:"protocol"`
102 SourcePort int `json:"sourcePort"`
103 TargetService string `json:"targetService"`
104 TargetPort int `json:"targetPort"`
105}
106
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400107func extractAllocateReq(r io.Reader) (allocateReq, error) {
108 var req allocateReq
109 if err := json.NewDecoder(r).Decode(&req); err != nil {
110 return allocateReq{}, err
111 }
112 req.Protocol = strings.ToLower(req.Protocol)
113 if req.Protocol != "tcp" && req.Protocol != "udp" {
114 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
115 }
116 return req, nil
117}
118
giocdfa3722024-06-13 20:10:14 +0400119func extractRemoveReq(r io.Reader) (removeReq, error) {
120 var req removeReq
121 if err := json.NewDecoder(r).Decode(&req); err != nil {
122 return removeReq{}, err
123 }
124 req.Protocol = strings.ToLower(req.Protocol)
125 if req.Protocol != "tcp" && req.Protocol != "udp" {
126 return removeReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
127 }
128 return req, nil
129}
130
gioefa0ed42024-06-13 12:31:43 +0400131type reserveResp struct {
132 Port int `json:"port"`
133 Secret string `json:"secret"`
134}
135
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400136func extractPorts(rel map[string]any) (map[string]any, map[string]any, error) {
137 spec, ok := rel["spec"]
138 if !ok {
139 return nil, nil, fmt.Errorf("spec not found")
140 }
141 specM, ok := spec.(map[string]any)
142 if !ok {
143 return nil, nil, fmt.Errorf("spec is not a map")
144 }
145 values, ok := specM["values"]
146 if !ok {
147 return nil, nil, fmt.Errorf("spec.values not found")
148 }
149 valuesM, ok := values.(map[string]any)
150 if !ok {
151 return nil, nil, fmt.Errorf("spec.values is not a map")
152 }
153 tcp, ok := valuesM["tcp"]
154 if !ok {
155 tcp = map[string]any{}
156 valuesM["tcp"] = tcp
157 }
158 udp, ok := valuesM["udp"]
159 if !ok {
160 udp = map[string]any{}
161 valuesM["udp"] = udp
162 }
163 tcpM, ok := tcp.(map[string]any)
164 if !ok {
165 return nil, nil, fmt.Errorf("spec.values.tcp is not a map")
166 }
167 udpM, ok := udp.(map[string]any)
168 if !ok {
169 return nil, nil, fmt.Errorf("spec.values.udp is not a map")
170 }
171 return tcpM, udpM, nil
172}
173
174func addPort(pm map[string]any, req allocateReq) error {
175 sourcePortStr := strconv.Itoa(req.SourcePort)
giobbc6fad2024-04-12 15:53:05 +0400176 if _, ok := pm[sourcePortStr]; ok || req.SourcePort == 80 || req.SourcePort == 443 || req.SourcePort == 22 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400177 return fmt.Errorf("port %d is already taken", req.SourcePort)
178 }
179 pm[sourcePortStr] = fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort)
180 return nil
181}
182
giocdfa3722024-06-13 20:10:14 +0400183func removePort(pm map[string]any, req removeReq) error {
184 sourcePortStr := strconv.Itoa(req.SourcePort)
185 if _, ok := pm[sourcePortStr]; !ok {
186 return fmt.Errorf("port %d is not open to remove", req.SourcePort)
187 }
188 delete(pm, sourcePortStr)
189 return nil
190}
191
gioefa0ed42024-06-13 12:31:43 +0400192const start = 49152
193const end = 65535
194
195func reservePort(pm map[string]struct{}, reserve map[int]string) (int, error) {
196 for i := 0; i < 3; i++ {
197 r, err := rand.Int(rand.Reader, big.NewInt(end-start))
198 if err != nil {
199 return -1, err
200 }
201 p := start + int(r.Int64())
202 ps := strconv.Itoa(p)
203 if _, ok := pm[ps]; !ok {
Davit Tabidze5bea96a2024-06-17 21:25:29 +0400204 if _, ok := reserve[p]; !ok {
205 return p, nil
206 }
gioefa0ed42024-06-13 12:31:43 +0400207 }
208 }
209 return -1, fmt.Errorf("could not generate random port")
210}
211
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400212func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
213 if r.Method != http.MethodPost {
214 http.Error(w, "only post method is supported", http.StatusBadRequest)
215 return
216 }
217 req, err := extractAllocateReq(r.Body)
218 if err != nil {
219 http.Error(w, err.Error(), http.StatusBadRequest)
220 return
221 }
gioefa0ed42024-06-13 12:31:43 +0400222 s.l.Lock()
223 defer s.l.Unlock()
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400224 ingressRel, err := s.client.ReadRelease()
225 if err != nil {
226 http.Error(w, err.Error(), http.StatusInternalServerError)
227 return
228 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400229 tcp, udp, err := extractPorts(ingressRel)
230 if err != nil {
231 http.Error(w, err.Error(), http.StatusInternalServerError)
232 return
233 }
gioefa0ed42024-06-13 12:31:43 +0400234 if val, ok := s.reserve[req.SourcePort]; !ok || val != req.Secret {
235 http.Error(w, "invalid secret", http.StatusBadRequest)
236 return
237 } else {
238 delete(s.reserve, req.SourcePort)
239 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400240 switch req.Protocol {
241 case "tcp":
242 if err := addPort(tcp, req); err != nil {
243 http.Error(w, err.Error(), http.StatusConflict)
244 return
245 }
246 case "udp":
247 if err := addPort(udp, req); err != nil {
248 http.Error(w, err.Error(), http.StatusConflict)
249 return
250 }
251 default:
252 panic("MUST NOT REACH")
253 }
254 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
255 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
259}
260
gioefa0ed42024-06-13 12:31:43 +0400261func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
262 if r.Method != http.MethodPost {
263 http.Error(w, "only post method is supported", http.StatusBadRequest)
264 return
265 }
266 s.l.Lock()
267 defer s.l.Unlock()
268 ingressRel, err := s.client.ReadRelease()
269 if err != nil {
270 http.Error(w, err.Error(), http.StatusInternalServerError)
271 return
272 }
273 tcp, udp, err := extractPorts(ingressRel)
274 if err != nil {
275 http.Error(w, err.Error(), http.StatusInternalServerError)
276 return
277 }
278 var port int
279 used := map[string]struct{}{}
280 for p, _ := range tcp {
281 used[p] = struct{}{}
282 }
283 for p, _ := range udp {
284 used[p] = struct{}{}
285 }
286 if port, err = reservePort(used, s.reserve); err != nil {
287 http.Error(w, err.Error(), http.StatusInternalServerError)
288 return
289 }
Davit Tabidze6bf29832024-06-17 16:51:54 +0400290 secret, err := generateSecret()
291 if err != nil {
292 http.Error(w, err.Error(), http.StatusInternalServerError)
293 return
294 }
gioefa0ed42024-06-13 12:31:43 +0400295 s.reserve[port] = secret
296 go func() {
297 time.Sleep(30 * time.Minute)
298 s.l.Lock()
299 defer s.l.Unlock()
300 delete(s.reserve, port)
301 }()
302 resp := reserveResp{port, secret}
303 if err := json.NewEncoder(w).Encode(resp); err != nil {
304 http.Error(w, err.Error(), http.StatusInternalServerError)
305 return
306 }
307}
308
giocdfa3722024-06-13 20:10:14 +0400309func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
310 if r.Method != http.MethodPost {
311 http.Error(w, "only post method is supported", http.StatusBadRequest)
312 return
313 }
314 req, err := extractRemoveReq(r.Body)
315 if err != nil {
316 http.Error(w, err.Error(), http.StatusBadRequest)
317 return
318 }
319 s.l.Lock()
320 defer s.l.Unlock()
321 ingressRel, err := s.client.ReadRelease()
322 if err != nil {
323 http.Error(w, err.Error(), http.StatusInternalServerError)
324 return
325 }
326 tcp, udp, err := extractPorts(ingressRel)
327 if err != nil {
328 http.Error(w, err.Error(), http.StatusInternalServerError)
329 return
330 }
331 switch req.Protocol {
332 case "tcp":
333 if err := removePort(tcp, req); err != nil {
334 http.Error(w, err.Error(), http.StatusConflict)
335 return
336 }
337 case "udp":
338 if err := removePort(udp, req); err != nil {
339 http.Error(w, err.Error(), http.StatusConflict)
340 return
341 }
342 default:
343 panic("MUST NOT REACH")
344 }
345 commitMsg := fmt.Sprintf("ingress: remove port map %d %s", req.SourcePort, req.Protocol)
346 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
347 http.Error(w, err.Error(), http.StatusInternalServerError)
348 return
349 }
350 delete(s.reserve, req.SourcePort)
351}
352
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400353// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400354func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400355 sshKey, err := os.ReadFile(keyPath)
356 if err != nil {
357 return nil, err
358 }
359 signer, err := ssh.ParsePrivateKey(sshKey)
360 if err != nil {
361 return nil, err
362 }
363 repoAddr, err := soft.ParseRepositoryAddress(addr)
364 if err != nil {
365 return nil, err
366 }
367 repo, err := soft.CloneRepository(repoAddr, signer)
368 if err != nil {
369 return nil, err
370 }
gioff2a29a2024-05-01 17:06:42 +0400371 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400372}
373
Davit Tabidze6bf29832024-06-17 16:51:54 +0400374func generateSecret() (string, error) {
375 b := make([]byte, secretLength)
376 _, err := rand.Read(b)
377 if err != nil {
378 return "", fmt.Errorf("error generating secret: %v", err)
379 }
giob1c4e542024-07-15 12:10:52 +0400380 return base64.StdEncoding.EncodeToString(b), nil
gioefa0ed42024-06-13 12:31:43 +0400381}
382
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400383func main() {
384 flag.Parse()
385 repo, err := createRepoClient(*repoAddr, *sshKey)
386 if err != nil {
387 log.Fatal(err)
388 }
389 s := newServer(
390 *port,
391 &repoClient{repo, *ingressNginxPath},
392 )
393 log.Fatal(s.Start())
394}