blob: 861b1dd0321479ce9c68e7227082b1b11cc69a93 [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 {
74 s.r.HandleFunc("/api/allocate", s.handleAllocate)
gioefa0ed42024-06-13 12:31:43 +040075 s.r.HandleFunc("/api/reserve", s.handleReserve)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040076 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
77 return err
78 }
79 return nil
80}
81
82func (s *server) Close() error {
83 return s.s.Close()
84}
85
86type allocateReq struct {
87 Protocol string `json:"protocol"`
88 SourcePort int `json:"sourcePort"`
89 TargetService string `json:"targetService"`
90 TargetPort int `json:"targetPort"`
gioefa0ed42024-06-13 12:31:43 +040091 Secret string `json:"secret,omitempty"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040092}
93
94func extractAllocateReq(r io.Reader) (allocateReq, error) {
95 var req allocateReq
96 if err := json.NewDecoder(r).Decode(&req); err != nil {
97 return allocateReq{}, err
98 }
99 req.Protocol = strings.ToLower(req.Protocol)
100 if req.Protocol != "tcp" && req.Protocol != "udp" {
101 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
102 }
103 return req, nil
104}
105
gioefa0ed42024-06-13 12:31:43 +0400106type reserveResp struct {
107 Port int `json:"port"`
108 Secret string `json:"secret"`
109}
110
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400111func extractPorts(rel map[string]any) (map[string]any, map[string]any, error) {
112 spec, ok := rel["spec"]
113 if !ok {
114 return nil, nil, fmt.Errorf("spec not found")
115 }
116 specM, ok := spec.(map[string]any)
117 if !ok {
118 return nil, nil, fmt.Errorf("spec is not a map")
119 }
120 values, ok := specM["values"]
121 if !ok {
122 return nil, nil, fmt.Errorf("spec.values not found")
123 }
124 valuesM, ok := values.(map[string]any)
125 if !ok {
126 return nil, nil, fmt.Errorf("spec.values is not a map")
127 }
128 tcp, ok := valuesM["tcp"]
129 if !ok {
130 tcp = map[string]any{}
131 valuesM["tcp"] = tcp
132 }
133 udp, ok := valuesM["udp"]
134 if !ok {
135 udp = map[string]any{}
136 valuesM["udp"] = udp
137 }
138 tcpM, ok := tcp.(map[string]any)
139 if !ok {
140 return nil, nil, fmt.Errorf("spec.values.tcp is not a map")
141 }
142 udpM, ok := udp.(map[string]any)
143 if !ok {
144 return nil, nil, fmt.Errorf("spec.values.udp is not a map")
145 }
146 return tcpM, udpM, nil
147}
148
149func addPort(pm map[string]any, req allocateReq) error {
150 sourcePortStr := strconv.Itoa(req.SourcePort)
giobbc6fad2024-04-12 15:53:05 +0400151 if _, ok := pm[sourcePortStr]; ok || req.SourcePort == 80 || req.SourcePort == 443 || req.SourcePort == 22 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400152 return fmt.Errorf("port %d is already taken", req.SourcePort)
153 }
154 pm[sourcePortStr] = fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort)
155 return nil
156}
157
gioefa0ed42024-06-13 12:31:43 +0400158const start = 49152
159const end = 65535
160
161func reservePort(pm map[string]struct{}, reserve map[int]string) (int, error) {
162 for i := 0; i < 3; i++ {
163 r, err := rand.Int(rand.Reader, big.NewInt(end-start))
164 if err != nil {
165 return -1, err
166 }
167 p := start + int(r.Int64())
168 ps := strconv.Itoa(p)
169 if _, ok := pm[ps]; !ok {
170 return p, nil
171 }
172 }
173 return -1, fmt.Errorf("could not generate random port")
174}
175
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400176func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
177 if r.Method != http.MethodPost {
178 http.Error(w, "only post method is supported", http.StatusBadRequest)
179 return
180 }
181 req, err := extractAllocateReq(r.Body)
182 if err != nil {
183 http.Error(w, err.Error(), http.StatusBadRequest)
184 return
185 }
gioefa0ed42024-06-13 12:31:43 +0400186 if req.Secret != "" {
187 http.Error(w, "secret missing", http.StatusBadRequest)
188 return
189 }
190 s.l.Lock()
191 defer s.l.Unlock()
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400192 ingressRel, err := s.client.ReadRelease()
193 if err != nil {
194 http.Error(w, err.Error(), http.StatusInternalServerError)
195 return
196 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400197 tcp, udp, err := extractPorts(ingressRel)
198 if err != nil {
199 http.Error(w, err.Error(), http.StatusInternalServerError)
200 return
201 }
gioefa0ed42024-06-13 12:31:43 +0400202 if val, ok := s.reserve[req.SourcePort]; !ok || val != req.Secret {
203 http.Error(w, "invalid secret", http.StatusBadRequest)
204 return
205 } else {
206 delete(s.reserve, req.SourcePort)
207 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400208 switch req.Protocol {
209 case "tcp":
210 if err := addPort(tcp, req); err != nil {
211 http.Error(w, err.Error(), http.StatusConflict)
212 return
213 }
214 case "udp":
215 if err := addPort(udp, req); err != nil {
216 http.Error(w, err.Error(), http.StatusConflict)
217 return
218 }
219 default:
220 panic("MUST NOT REACH")
221 }
222 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
223 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
227}
228
gioefa0ed42024-06-13 12:31:43 +0400229func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
230 if r.Method != http.MethodPost {
231 http.Error(w, "only post method is supported", http.StatusBadRequest)
232 return
233 }
234 s.l.Lock()
235 defer s.l.Unlock()
236 ingressRel, err := s.client.ReadRelease()
237 if err != nil {
238 http.Error(w, err.Error(), http.StatusInternalServerError)
239 return
240 }
241 tcp, udp, err := extractPorts(ingressRel)
242 if err != nil {
243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 return
245 }
246 var port int
247 used := map[string]struct{}{}
248 for p, _ := range tcp {
249 used[p] = struct{}{}
250 }
251 for p, _ := range udp {
252 used[p] = struct{}{}
253 }
254 if port, err = reservePort(used, s.reserve); err != nil {
255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
257 }
258 secret := generateSecret()
259 s.reserve[port] = secret
260 go func() {
261 time.Sleep(30 * time.Minute)
262 s.l.Lock()
263 defer s.l.Unlock()
264 delete(s.reserve, port)
265 }()
266 resp := reserveResp{port, secret}
267 if err := json.NewEncoder(w).Encode(resp); err != nil {
268 http.Error(w, err.Error(), http.StatusInternalServerError)
269 return
270 }
271}
272
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400273// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400274func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400275 sshKey, err := os.ReadFile(keyPath)
276 if err != nil {
277 return nil, err
278 }
279 signer, err := ssh.ParsePrivateKey(sshKey)
280 if err != nil {
281 return nil, err
282 }
283 repoAddr, err := soft.ParseRepositoryAddress(addr)
284 if err != nil {
285 return nil, err
286 }
287 repo, err := soft.CloneRepository(repoAddr, signer)
288 if err != nil {
289 return nil, err
290 }
gioff2a29a2024-05-01 17:06:42 +0400291 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400292}
293
gioefa0ed42024-06-13 12:31:43 +0400294func generateSecret() string {
295 // TODO(gio): implement
296 return "foo"
297}
298
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400299func main() {
300 flag.Parse()
301 repo, err := createRepoClient(*repoAddr, *sshKey)
302 if err != nil {
303 log.Fatal(err)
304 }
305 s := newServer(
306 *port,
307 &repoClient{repo, *ingressNginxPath},
308 )
309 log.Fatal(s.Start())
310}