blob: 3d69ab92ad209dbfe3c200c82f6a41eb861e3850 [file] [log] [blame]
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04001package main
2
3import (
gioefa0ed42024-06-13 12:31:43 +04004 "crypto/rand"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04005 "encoding/json"
6 "flag"
7 "fmt"
8 "io"
9 "log"
gioefa0ed42024-06-13 12:31:43 +040010 "math/big"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040011 "net/http"
12 "os"
13 "strconv"
14 "strings"
gioefa0ed42024-06-13 12:31:43 +040015 "sync"
16 "time"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040017
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040018 "github.com/giolekva/pcloud/core/installer/soft"
19
20 "golang.org/x/crypto/ssh"
21)
22
Davit Tabidze6bf29832024-06-17 16:51:54 +040023const (
24 secretLength = 20
25)
26
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040027var port = flag.Int("port", 8080, "Port to listen on")
28var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
29var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
30var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
31
32type client interface {
33 ReadRelease() (map[string]any, error)
34 WriteRelease(rel map[string]any, meta string) error
35}
36
37type repoClient struct {
gioe72b54f2024-04-22 10:44:41 +040038 repo soft.RepoIO
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040039 path string
40}
41
42func (c *repoClient) ReadRelease() (map[string]any, error) {
43 if err := c.repo.Pull(); err != nil {
44 return nil, err
45 }
gioff2a29a2024-05-01 17:06:42 +040046 ingressRel := map[string]any{}
47 if err := soft.ReadYaml(c.repo, c.path, &ingressRel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040048 return nil, err
49 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040050 return ingressRel, nil
51}
52
53func (c *repoClient) WriteRelease(rel map[string]any, meta string) error {
gioff2a29a2024-05-01 17:06:42 +040054 if err := soft.WriteYaml(c.repo, c.path, rel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040055 return err
56 }
57 return c.repo.CommitAndPush(meta)
58}
59
60type server struct {
gioefa0ed42024-06-13 12:31:43 +040061 l sync.Locker
62 s *http.Server
63 r *http.ServeMux
64 client client
65 reserve map[int]string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040066}
67
68func newServer(port int, client client) *server {
69 r := http.NewServeMux()
70 s := &http.Server{
71 Addr: fmt.Sprintf(":%d", port),
72 Handler: r,
73 }
gioefa0ed42024-06-13 12:31:43 +040074 return &server{&sync.Mutex{}, s, r, client, make(map[int]string)}
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040075}
76
77func (s *server) Start() error {
gioefa0ed42024-06-13 12:31:43 +040078 s.r.HandleFunc("/api/reserve", s.handleReserve)
giocdfa3722024-06-13 20:10:14 +040079 s.r.HandleFunc("/api/allocate", s.handleAllocate)
80 s.r.HandleFunc("/api/remove", s.handleRemove)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040081 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
82 return err
83 }
84 return nil
85}
86
87func (s *server) Close() error {
88 return s.s.Close()
89}
90
91type allocateReq struct {
92 Protocol string `json:"protocol"`
93 SourcePort int `json:"sourcePort"`
94 TargetService string `json:"targetService"`
95 TargetPort int `json:"targetPort"`
giobd7ab0b2024-06-17 12:55:17 +040096 Secret string `json:"secret"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040097}
98
giocdfa3722024-06-13 20:10:14 +040099type removeReq struct {
100 Protocol string `json:"protocol"`
101 SourcePort int `json:"sourcePort"`
102 TargetService string `json:"targetService"`
103 TargetPort int `json:"targetPort"`
104}
105
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400106func extractAllocateReq(r io.Reader) (allocateReq, error) {
107 var req allocateReq
108 if err := json.NewDecoder(r).Decode(&req); err != nil {
109 return allocateReq{}, err
110 }
111 req.Protocol = strings.ToLower(req.Protocol)
112 if req.Protocol != "tcp" && req.Protocol != "udp" {
113 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
114 }
115 return req, nil
116}
117
giocdfa3722024-06-13 20:10:14 +0400118func extractRemoveReq(r io.Reader) (removeReq, error) {
119 var req removeReq
120 if err := json.NewDecoder(r).Decode(&req); err != nil {
121 return removeReq{}, err
122 }
123 req.Protocol = strings.ToLower(req.Protocol)
124 if req.Protocol != "tcp" && req.Protocol != "udp" {
125 return removeReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
126 }
127 return req, nil
128}
129
gioefa0ed42024-06-13 12:31:43 +0400130type reserveResp struct {
131 Port int `json:"port"`
132 Secret string `json:"secret"`
133}
134
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400135func extractPorts(rel map[string]any) (map[string]any, map[string]any, error) {
136 spec, ok := rel["spec"]
137 if !ok {
138 return nil, nil, fmt.Errorf("spec not found")
139 }
140 specM, ok := spec.(map[string]any)
141 if !ok {
142 return nil, nil, fmt.Errorf("spec is not a map")
143 }
144 values, ok := specM["values"]
145 if !ok {
146 return nil, nil, fmt.Errorf("spec.values not found")
147 }
148 valuesM, ok := values.(map[string]any)
149 if !ok {
150 return nil, nil, fmt.Errorf("spec.values is not a map")
151 }
152 tcp, ok := valuesM["tcp"]
153 if !ok {
154 tcp = map[string]any{}
155 valuesM["tcp"] = tcp
156 }
157 udp, ok := valuesM["udp"]
158 if !ok {
159 udp = map[string]any{}
160 valuesM["udp"] = udp
161 }
162 tcpM, ok := tcp.(map[string]any)
163 if !ok {
164 return nil, nil, fmt.Errorf("spec.values.tcp is not a map")
165 }
166 udpM, ok := udp.(map[string]any)
167 if !ok {
168 return nil, nil, fmt.Errorf("spec.values.udp is not a map")
169 }
170 return tcpM, udpM, nil
171}
172
173func addPort(pm map[string]any, req allocateReq) error {
174 sourcePortStr := strconv.Itoa(req.SourcePort)
giobbc6fad2024-04-12 15:53:05 +0400175 if _, ok := pm[sourcePortStr]; ok || req.SourcePort == 80 || req.SourcePort == 443 || req.SourcePort == 22 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400176 return fmt.Errorf("port %d is already taken", req.SourcePort)
177 }
178 pm[sourcePortStr] = fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort)
179 return nil
180}
181
giocdfa3722024-06-13 20:10:14 +0400182func removePort(pm map[string]any, req removeReq) error {
183 sourcePortStr := strconv.Itoa(req.SourcePort)
184 if _, ok := pm[sourcePortStr]; !ok {
185 return fmt.Errorf("port %d is not open to remove", req.SourcePort)
186 }
187 delete(pm, sourcePortStr)
188 return nil
189}
190
gioefa0ed42024-06-13 12:31:43 +0400191const start = 49152
192const end = 65535
193
194func reservePort(pm map[string]struct{}, reserve map[int]string) (int, error) {
195 for i := 0; i < 3; i++ {
196 r, err := rand.Int(rand.Reader, big.NewInt(end-start))
197 if err != nil {
198 return -1, err
199 }
200 p := start + int(r.Int64())
201 ps := strconv.Itoa(p)
202 if _, ok := pm[ps]; !ok {
Davit Tabidze5bea96a2024-06-17 21:25:29 +0400203 if _, ok := reserve[p]; !ok {
204 return p, nil
205 }
gioefa0ed42024-06-13 12:31:43 +0400206 }
207 }
208 return -1, fmt.Errorf("could not generate random port")
209}
210
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400211func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
212 if r.Method != http.MethodPost {
213 http.Error(w, "only post method is supported", http.StatusBadRequest)
214 return
215 }
216 req, err := extractAllocateReq(r.Body)
217 if err != nil {
218 http.Error(w, err.Error(), http.StatusBadRequest)
219 return
220 }
gioefa0ed42024-06-13 12:31:43 +0400221 s.l.Lock()
222 defer s.l.Unlock()
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400223 ingressRel, err := s.client.ReadRelease()
224 if err != nil {
225 http.Error(w, err.Error(), http.StatusInternalServerError)
226 return
227 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400228 tcp, udp, err := extractPorts(ingressRel)
229 if err != nil {
230 http.Error(w, err.Error(), http.StatusInternalServerError)
231 return
232 }
gioefa0ed42024-06-13 12:31:43 +0400233 if val, ok := s.reserve[req.SourcePort]; !ok || val != req.Secret {
234 http.Error(w, "invalid secret", http.StatusBadRequest)
235 return
236 } else {
237 delete(s.reserve, req.SourcePort)
238 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400239 switch req.Protocol {
240 case "tcp":
241 if err := addPort(tcp, req); err != nil {
242 http.Error(w, err.Error(), http.StatusConflict)
243 return
244 }
245 case "udp":
246 if err := addPort(udp, req); err != nil {
247 http.Error(w, err.Error(), http.StatusConflict)
248 return
249 }
250 default:
251 panic("MUST NOT REACH")
252 }
253 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
254 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
257 }
258}
259
gioefa0ed42024-06-13 12:31:43 +0400260func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
261 if r.Method != http.MethodPost {
262 http.Error(w, "only post method is supported", http.StatusBadRequest)
263 return
264 }
265 s.l.Lock()
266 defer s.l.Unlock()
267 ingressRel, err := s.client.ReadRelease()
268 if err != nil {
269 http.Error(w, err.Error(), http.StatusInternalServerError)
270 return
271 }
272 tcp, udp, err := extractPorts(ingressRel)
273 if err != nil {
274 http.Error(w, err.Error(), http.StatusInternalServerError)
275 return
276 }
277 var port int
278 used := map[string]struct{}{}
279 for p, _ := range tcp {
280 used[p] = struct{}{}
281 }
282 for p, _ := range udp {
283 used[p] = struct{}{}
284 }
285 if port, err = reservePort(used, s.reserve); err != nil {
286 http.Error(w, err.Error(), http.StatusInternalServerError)
287 return
288 }
Davit Tabidze6bf29832024-06-17 16:51:54 +0400289 secret, err := generateSecret()
290 if err != nil {
291 http.Error(w, err.Error(), http.StatusInternalServerError)
292 return
293 }
gioefa0ed42024-06-13 12:31:43 +0400294 s.reserve[port] = secret
295 go func() {
296 time.Sleep(30 * time.Minute)
297 s.l.Lock()
298 defer s.l.Unlock()
299 delete(s.reserve, port)
300 }()
301 resp := reserveResp{port, secret}
302 if err := json.NewEncoder(w).Encode(resp); err != nil {
303 http.Error(w, err.Error(), http.StatusInternalServerError)
304 return
305 }
306}
307
giocdfa3722024-06-13 20:10:14 +0400308func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
309 if r.Method != http.MethodPost {
310 http.Error(w, "only post method is supported", http.StatusBadRequest)
311 return
312 }
313 req, err := extractRemoveReq(r.Body)
314 if err != nil {
315 http.Error(w, err.Error(), http.StatusBadRequest)
316 return
317 }
318 s.l.Lock()
319 defer s.l.Unlock()
320 ingressRel, err := s.client.ReadRelease()
321 if err != nil {
322 http.Error(w, err.Error(), http.StatusInternalServerError)
323 return
324 }
325 tcp, udp, err := extractPorts(ingressRel)
326 if err != nil {
327 http.Error(w, err.Error(), http.StatusInternalServerError)
328 return
329 }
330 switch req.Protocol {
331 case "tcp":
332 if err := removePort(tcp, req); err != nil {
333 http.Error(w, err.Error(), http.StatusConflict)
334 return
335 }
336 case "udp":
337 if err := removePort(udp, req); err != nil {
338 http.Error(w, err.Error(), http.StatusConflict)
339 return
340 }
341 default:
342 panic("MUST NOT REACH")
343 }
344 commitMsg := fmt.Sprintf("ingress: remove port map %d %s", req.SourcePort, req.Protocol)
345 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
346 http.Error(w, err.Error(), http.StatusInternalServerError)
347 return
348 }
349 delete(s.reserve, req.SourcePort)
350}
351
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400352// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400353func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400354 sshKey, err := os.ReadFile(keyPath)
355 if err != nil {
356 return nil, err
357 }
358 signer, err := ssh.ParsePrivateKey(sshKey)
359 if err != nil {
360 return nil, err
361 }
362 repoAddr, err := soft.ParseRepositoryAddress(addr)
363 if err != nil {
364 return nil, err
365 }
366 repo, err := soft.CloneRepository(repoAddr, signer)
367 if err != nil {
368 return nil, err
369 }
gioff2a29a2024-05-01 17:06:42 +0400370 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400371}
372
Davit Tabidze6bf29832024-06-17 16:51:54 +0400373func generateSecret() (string, error) {
374 b := make([]byte, secretLength)
375 _, err := rand.Read(b)
376 if err != nil {
377 return "", fmt.Errorf("error generating secret: %v", err)
378 }
379 return string(b), nil
gioefa0ed42024-06-13 12:31:43 +0400380}
381
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400382func main() {
383 flag.Parse()
384 repo, err := createRepoClient(*repoAddr, *sshKey)
385 if err != nil {
386 log.Fatal(err)
387 }
388 s := newServer(
389 *port,
390 &repoClient{repo, *ingressNginxPath},
391 )
392 log.Fatal(s.Start())
393}