blob: d99550b0d73066f5bc95528c658fa24188339f64 [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
23var port = flag.Int("port", 8080, "Port to listen on")
24var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
25var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
26var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
27
28type client interface {
29 ReadRelease() (map[string]any, error)
30 WriteRelease(rel map[string]any, meta string) error
31}
32
33type repoClient struct {
gioe72b54f2024-04-22 10:44:41 +040034 repo soft.RepoIO
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040035 path string
36}
37
38func (c *repoClient) ReadRelease() (map[string]any, error) {
39 if err := c.repo.Pull(); err != nil {
40 return nil, err
41 }
gioff2a29a2024-05-01 17:06:42 +040042 ingressRel := map[string]any{}
43 if err := soft.ReadYaml(c.repo, c.path, &ingressRel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040044 return nil, err
45 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040046 return ingressRel, nil
47}
48
49func (c *repoClient) WriteRelease(rel map[string]any, meta string) error {
gioff2a29a2024-05-01 17:06:42 +040050 if err := soft.WriteYaml(c.repo, c.path, rel); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040051 return err
52 }
53 return c.repo.CommitAndPush(meta)
54}
55
56type server struct {
gioefa0ed42024-06-13 12:31:43 +040057 l sync.Locker
58 s *http.Server
59 r *http.ServeMux
60 client client
61 reserve map[int]string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040062}
63
64func newServer(port int, client client) *server {
65 r := http.NewServeMux()
66 s := &http.Server{
67 Addr: fmt.Sprintf(":%d", port),
68 Handler: r,
69 }
gioefa0ed42024-06-13 12:31:43 +040070 return &server{&sync.Mutex{}, s, r, client, make(map[int]string)}
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040071}
72
73func (s *server) Start() error {
gioefa0ed42024-06-13 12:31:43 +040074 s.r.HandleFunc("/api/reserve", s.handleReserve)
giocdfa3722024-06-13 20:10:14 +040075 s.r.HandleFunc("/api/allocate", s.handleAllocate)
76 s.r.HandleFunc("/api/remove", s.handleRemove)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040077 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
78 return err
79 }
80 return nil
81}
82
83func (s *server) Close() error {
84 return s.s.Close()
85}
86
87type allocateReq struct {
88 Protocol string `json:"protocol"`
89 SourcePort int `json:"sourcePort"`
90 TargetService string `json:"targetService"`
91 TargetPort int `json:"targetPort"`
giobd7ab0b2024-06-17 12:55:17 +040092 Secret string `json:"secret"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040093}
94
giocdfa3722024-06-13 20:10:14 +040095type removeReq struct {
96 Protocol string `json:"protocol"`
97 SourcePort int `json:"sourcePort"`
98 TargetService string `json:"targetService"`
99 TargetPort int `json:"targetPort"`
100}
101
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400102func extractAllocateReq(r io.Reader) (allocateReq, error) {
103 var req allocateReq
104 if err := json.NewDecoder(r).Decode(&req); err != nil {
105 return allocateReq{}, err
106 }
107 req.Protocol = strings.ToLower(req.Protocol)
108 if req.Protocol != "tcp" && req.Protocol != "udp" {
109 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
110 }
111 return req, nil
112}
113
giocdfa3722024-06-13 20:10:14 +0400114func extractRemoveReq(r io.Reader) (removeReq, error) {
115 var req removeReq
116 if err := json.NewDecoder(r).Decode(&req); err != nil {
117 return removeReq{}, err
118 }
119 req.Protocol = strings.ToLower(req.Protocol)
120 if req.Protocol != "tcp" && req.Protocol != "udp" {
121 return removeReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
122 }
123 return req, nil
124}
125
gioefa0ed42024-06-13 12:31:43 +0400126type reserveResp struct {
127 Port int `json:"port"`
128 Secret string `json:"secret"`
129}
130
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400131func extractPorts(rel map[string]any) (map[string]any, map[string]any, error) {
132 spec, ok := rel["spec"]
133 if !ok {
134 return nil, nil, fmt.Errorf("spec not found")
135 }
136 specM, ok := spec.(map[string]any)
137 if !ok {
138 return nil, nil, fmt.Errorf("spec is not a map")
139 }
140 values, ok := specM["values"]
141 if !ok {
142 return nil, nil, fmt.Errorf("spec.values not found")
143 }
144 valuesM, ok := values.(map[string]any)
145 if !ok {
146 return nil, nil, fmt.Errorf("spec.values is not a map")
147 }
148 tcp, ok := valuesM["tcp"]
149 if !ok {
150 tcp = map[string]any{}
151 valuesM["tcp"] = tcp
152 }
153 udp, ok := valuesM["udp"]
154 if !ok {
155 udp = map[string]any{}
156 valuesM["udp"] = udp
157 }
158 tcpM, ok := tcp.(map[string]any)
159 if !ok {
160 return nil, nil, fmt.Errorf("spec.values.tcp is not a map")
161 }
162 udpM, ok := udp.(map[string]any)
163 if !ok {
164 return nil, nil, fmt.Errorf("spec.values.udp is not a map")
165 }
166 return tcpM, udpM, nil
167}
168
169func addPort(pm map[string]any, req allocateReq) error {
170 sourcePortStr := strconv.Itoa(req.SourcePort)
giobbc6fad2024-04-12 15:53:05 +0400171 if _, ok := pm[sourcePortStr]; ok || req.SourcePort == 80 || req.SourcePort == 443 || req.SourcePort == 22 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400172 return fmt.Errorf("port %d is already taken", req.SourcePort)
173 }
174 pm[sourcePortStr] = fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort)
175 return nil
176}
177
giocdfa3722024-06-13 20:10:14 +0400178func removePort(pm map[string]any, req removeReq) error {
179 sourcePortStr := strconv.Itoa(req.SourcePort)
180 if _, ok := pm[sourcePortStr]; !ok {
181 return fmt.Errorf("port %d is not open to remove", req.SourcePort)
182 }
183 delete(pm, sourcePortStr)
184 return nil
185}
186
gioefa0ed42024-06-13 12:31:43 +0400187const start = 49152
188const end = 65535
189
190func reservePort(pm map[string]struct{}, reserve map[int]string) (int, error) {
191 for i := 0; i < 3; i++ {
192 r, err := rand.Int(rand.Reader, big.NewInt(end-start))
193 if err != nil {
194 return -1, err
195 }
196 p := start + int(r.Int64())
197 ps := strconv.Itoa(p)
198 if _, ok := pm[ps]; !ok {
199 return p, nil
200 }
201 }
202 return -1, fmt.Errorf("could not generate random port")
203}
204
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400205func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
206 if r.Method != http.MethodPost {
207 http.Error(w, "only post method is supported", http.StatusBadRequest)
208 return
209 }
210 req, err := extractAllocateReq(r.Body)
211 if err != nil {
212 http.Error(w, err.Error(), http.StatusBadRequest)
213 return
214 }
gioefa0ed42024-06-13 12:31:43 +0400215 s.l.Lock()
216 defer s.l.Unlock()
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400217 ingressRel, err := s.client.ReadRelease()
218 if err != nil {
219 http.Error(w, err.Error(), http.StatusInternalServerError)
220 return
221 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400222 tcp, udp, err := extractPorts(ingressRel)
223 if err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
gioefa0ed42024-06-13 12:31:43 +0400227 if val, ok := s.reserve[req.SourcePort]; !ok || val != req.Secret {
228 http.Error(w, "invalid secret", http.StatusBadRequest)
229 return
230 } else {
231 delete(s.reserve, req.SourcePort)
232 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400233 switch req.Protocol {
234 case "tcp":
235 if err := addPort(tcp, req); err != nil {
236 http.Error(w, err.Error(), http.StatusConflict)
237 return
238 }
239 case "udp":
240 if err := addPort(udp, req); err != nil {
241 http.Error(w, err.Error(), http.StatusConflict)
242 return
243 }
244 default:
245 panic("MUST NOT REACH")
246 }
247 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
248 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
249 http.Error(w, err.Error(), http.StatusInternalServerError)
250 return
251 }
252}
253
gioefa0ed42024-06-13 12:31:43 +0400254func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
255 if r.Method != http.MethodPost {
256 http.Error(w, "only post method is supported", http.StatusBadRequest)
257 return
258 }
259 s.l.Lock()
260 defer s.l.Unlock()
261 ingressRel, err := s.client.ReadRelease()
262 if err != nil {
263 http.Error(w, err.Error(), http.StatusInternalServerError)
264 return
265 }
266 tcp, udp, err := extractPorts(ingressRel)
267 if err != nil {
268 http.Error(w, err.Error(), http.StatusInternalServerError)
269 return
270 }
271 var port int
272 used := map[string]struct{}{}
273 for p, _ := range tcp {
274 used[p] = struct{}{}
275 }
276 for p, _ := range udp {
277 used[p] = struct{}{}
278 }
279 if port, err = reservePort(used, s.reserve); err != nil {
280 http.Error(w, err.Error(), http.StatusInternalServerError)
281 return
282 }
283 secret := generateSecret()
284 s.reserve[port] = secret
285 go func() {
286 time.Sleep(30 * time.Minute)
287 s.l.Lock()
288 defer s.l.Unlock()
289 delete(s.reserve, port)
290 }()
291 resp := reserveResp{port, secret}
292 if err := json.NewEncoder(w).Encode(resp); err != nil {
293 http.Error(w, err.Error(), http.StatusInternalServerError)
294 return
295 }
296}
297
giocdfa3722024-06-13 20:10:14 +0400298func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
299 if r.Method != http.MethodPost {
300 http.Error(w, "only post method is supported", http.StatusBadRequest)
301 return
302 }
303 req, err := extractRemoveReq(r.Body)
304 if err != nil {
305 http.Error(w, err.Error(), http.StatusBadRequest)
306 return
307 }
308 s.l.Lock()
309 defer s.l.Unlock()
310 ingressRel, err := s.client.ReadRelease()
311 if err != nil {
312 http.Error(w, err.Error(), http.StatusInternalServerError)
313 return
314 }
315 tcp, udp, err := extractPorts(ingressRel)
316 if err != nil {
317 http.Error(w, err.Error(), http.StatusInternalServerError)
318 return
319 }
320 switch req.Protocol {
321 case "tcp":
322 if err := removePort(tcp, req); err != nil {
323 http.Error(w, err.Error(), http.StatusConflict)
324 return
325 }
326 case "udp":
327 if err := removePort(udp, req); err != nil {
328 http.Error(w, err.Error(), http.StatusConflict)
329 return
330 }
331 default:
332 panic("MUST NOT REACH")
333 }
334 commitMsg := fmt.Sprintf("ingress: remove port map %d %s", req.SourcePort, req.Protocol)
335 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
336 http.Error(w, err.Error(), http.StatusInternalServerError)
337 return
338 }
339 delete(s.reserve, req.SourcePort)
340}
341
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400342// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400343func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400344 sshKey, err := os.ReadFile(keyPath)
345 if err != nil {
346 return nil, err
347 }
348 signer, err := ssh.ParsePrivateKey(sshKey)
349 if err != nil {
350 return nil, err
351 }
352 repoAddr, err := soft.ParseRepositoryAddress(addr)
353 if err != nil {
354 return nil, err
355 }
356 repo, err := soft.CloneRepository(repoAddr, signer)
357 if err != nil {
358 return nil, err
359 }
gioff2a29a2024-05-01 17:06:42 +0400360 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400361}
362
gioefa0ed42024-06-13 12:31:43 +0400363func generateSecret() string {
364 // TODO(gio): implement
365 return "foo"
366}
367
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400368func main() {
369 flag.Parse()
370 repo, err := createRepoClient(*repoAddr, *sshKey)
371 if err != nil {
372 log.Fatal(err)
373 }
374 s := newServer(
375 *port,
376 &repoClient{repo, *ingressNginxPath},
377 )
378 log.Fatal(s.Start())
379}