blob: 6b4fd472483eeb7da9705e496e9fe13d9a6222b2 [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 {
203 return p, nil
204 }
205 }
206 return -1, fmt.Errorf("could not generate random port")
207}
208
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400209func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
210 if r.Method != http.MethodPost {
211 http.Error(w, "only post method is supported", http.StatusBadRequest)
212 return
213 }
214 req, err := extractAllocateReq(r.Body)
215 if err != nil {
216 http.Error(w, err.Error(), http.StatusBadRequest)
217 return
218 }
gioefa0ed42024-06-13 12:31:43 +0400219 s.l.Lock()
220 defer s.l.Unlock()
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400221 ingressRel, err := s.client.ReadRelease()
222 if err != nil {
223 http.Error(w, err.Error(), http.StatusInternalServerError)
224 return
225 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400226 tcp, udp, err := extractPorts(ingressRel)
227 if err != nil {
228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 return
230 }
gioefa0ed42024-06-13 12:31:43 +0400231 if val, ok := s.reserve[req.SourcePort]; !ok || val != req.Secret {
232 http.Error(w, "invalid secret", http.StatusBadRequest)
233 return
234 } else {
235 delete(s.reserve, req.SourcePort)
236 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400237 switch req.Protocol {
238 case "tcp":
239 if err := addPort(tcp, req); err != nil {
240 http.Error(w, err.Error(), http.StatusConflict)
241 return
242 }
243 case "udp":
244 if err := addPort(udp, req); err != nil {
245 http.Error(w, err.Error(), http.StatusConflict)
246 return
247 }
248 default:
249 panic("MUST NOT REACH")
250 }
251 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
252 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
253 http.Error(w, err.Error(), http.StatusInternalServerError)
254 return
255 }
256}
257
gioefa0ed42024-06-13 12:31:43 +0400258func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
259 if r.Method != http.MethodPost {
260 http.Error(w, "only post method is supported", http.StatusBadRequest)
261 return
262 }
263 s.l.Lock()
264 defer s.l.Unlock()
265 ingressRel, err := s.client.ReadRelease()
266 if err != nil {
267 http.Error(w, err.Error(), http.StatusInternalServerError)
268 return
269 }
270 tcp, udp, err := extractPorts(ingressRel)
271 if err != nil {
272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
274 }
275 var port int
276 used := map[string]struct{}{}
277 for p, _ := range tcp {
278 used[p] = struct{}{}
279 }
280 for p, _ := range udp {
281 used[p] = struct{}{}
282 }
283 if port, err = reservePort(used, s.reserve); err != nil {
284 http.Error(w, err.Error(), http.StatusInternalServerError)
285 return
286 }
Davit Tabidze6bf29832024-06-17 16:51:54 +0400287 secret, err := generateSecret()
288 if err != nil {
289 http.Error(w, err.Error(), http.StatusInternalServerError)
290 return
291 }
gioefa0ed42024-06-13 12:31:43 +0400292 s.reserve[port] = secret
293 go func() {
294 time.Sleep(30 * time.Minute)
295 s.l.Lock()
296 defer s.l.Unlock()
297 delete(s.reserve, port)
298 }()
299 resp := reserveResp{port, secret}
300 if err := json.NewEncoder(w).Encode(resp); err != nil {
301 http.Error(w, err.Error(), http.StatusInternalServerError)
302 return
303 }
304}
305
giocdfa3722024-06-13 20:10:14 +0400306func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
307 if r.Method != http.MethodPost {
308 http.Error(w, "only post method is supported", http.StatusBadRequest)
309 return
310 }
311 req, err := extractRemoveReq(r.Body)
312 if err != nil {
313 http.Error(w, err.Error(), http.StatusBadRequest)
314 return
315 }
316 s.l.Lock()
317 defer s.l.Unlock()
318 ingressRel, err := s.client.ReadRelease()
319 if err != nil {
320 http.Error(w, err.Error(), http.StatusInternalServerError)
321 return
322 }
323 tcp, udp, err := extractPorts(ingressRel)
324 if err != nil {
325 http.Error(w, err.Error(), http.StatusInternalServerError)
326 return
327 }
328 switch req.Protocol {
329 case "tcp":
330 if err := removePort(tcp, req); err != nil {
331 http.Error(w, err.Error(), http.StatusConflict)
332 return
333 }
334 case "udp":
335 if err := removePort(udp, req); err != nil {
336 http.Error(w, err.Error(), http.StatusConflict)
337 return
338 }
339 default:
340 panic("MUST NOT REACH")
341 }
342 commitMsg := fmt.Sprintf("ingress: remove port map %d %s", req.SourcePort, req.Protocol)
343 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
344 http.Error(w, err.Error(), http.StatusInternalServerError)
345 return
346 }
347 delete(s.reserve, req.SourcePort)
348}
349
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400350// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400351func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400352 sshKey, err := os.ReadFile(keyPath)
353 if err != nil {
354 return nil, err
355 }
356 signer, err := ssh.ParsePrivateKey(sshKey)
357 if err != nil {
358 return nil, err
359 }
360 repoAddr, err := soft.ParseRepositoryAddress(addr)
361 if err != nil {
362 return nil, err
363 }
364 repo, err := soft.CloneRepository(repoAddr, signer)
365 if err != nil {
366 return nil, err
367 }
gioff2a29a2024-05-01 17:06:42 +0400368 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400369}
370
Davit Tabidze6bf29832024-06-17 16:51:54 +0400371func generateSecret() (string, error) {
372 b := make([]byte, secretLength)
373 _, err := rand.Read(b)
374 if err != nil {
375 return "", fmt.Errorf("error generating secret: %v", err)
376 }
377 return string(b), nil
gioefa0ed42024-06-13 12:31:43 +0400378}
379
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400380func main() {
381 flag.Parse()
382 repo, err := createRepoClient(*repoAddr, *sshKey)
383 if err != nil {
384 log.Fatal(err)
385 }
386 s := newServer(
387 *port,
388 &repoClient{repo, *ingressNginxPath},
389 )
390 log.Fatal(s.Start())
391}