blob: 5b557de1c7f28a16cf689ee7b5c09c49b6fc0251 [file] [log] [blame]
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04001package main
2
3import (
4 "encoding/json"
5 "flag"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "os"
11 "strconv"
12 "strings"
13
14 "github.com/giolekva/pcloud/core/installer"
15 "github.com/giolekva/pcloud/core/installer/soft"
16
17 "golang.org/x/crypto/ssh"
18)
19
20var port = flag.Int("port", 8080, "Port to listen on")
21var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
22var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
23var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
24
25type client interface {
26 ReadRelease() (map[string]any, error)
27 WriteRelease(rel map[string]any, meta string) error
28}
29
30type repoClient struct {
gioe72b54f2024-04-22 10:44:41 +040031 repo soft.RepoIO
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040032 path string
33}
34
35func (c *repoClient) ReadRelease() (map[string]any, error) {
36 if err := c.repo.Pull(); err != nil {
37 return nil, err
38 }
39 rel, err := c.repo.ReadYaml(c.path)
40 if err != nil {
41 return nil, err
42 }
43 ingressRel, ok := rel.(map[string]any)
44 if !ok {
45 panic("MUST NOT REACH!")
46 }
47 return ingressRel, nil
48}
49
50func (c *repoClient) WriteRelease(rel map[string]any, meta string) error {
51 if err := c.repo.WriteYaml(c.path, rel); err != nil {
52 return err
53 }
54 return c.repo.CommitAndPush(meta)
55}
56
57type server struct {
58 s *http.Server
59 r *http.ServeMux
60 client client
61}
62
63func newServer(port int, client client) *server {
64 r := http.NewServeMux()
65 s := &http.Server{
66 Addr: fmt.Sprintf(":%d", port),
67 Handler: r,
68 }
69 return &server{s, r, client}
70}
71
72func (s *server) Start() error {
73 s.r.HandleFunc("/api/allocate", s.handleAllocate)
74 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
75 return err
76 }
77 return nil
78}
79
80func (s *server) Close() error {
81 return s.s.Close()
82}
83
84type allocateReq struct {
85 Protocol string `json:"protocol"`
86 SourcePort int `json:"sourcePort"`
87 TargetService string `json:"targetService"`
88 TargetPort int `json:"targetPort"`
89}
90
91func extractAllocateReq(r io.Reader) (allocateReq, error) {
92 var req allocateReq
93 if err := json.NewDecoder(r).Decode(&req); err != nil {
94 return allocateReq{}, err
95 }
96 req.Protocol = strings.ToLower(req.Protocol)
97 if req.Protocol != "tcp" && req.Protocol != "udp" {
98 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
99 }
100 return req, nil
101}
102
103func extractPorts(rel map[string]any) (map[string]any, map[string]any, error) {
104 spec, ok := rel["spec"]
105 if !ok {
106 return nil, nil, fmt.Errorf("spec not found")
107 }
108 specM, ok := spec.(map[string]any)
109 if !ok {
110 return nil, nil, fmt.Errorf("spec is not a map")
111 }
112 values, ok := specM["values"]
113 if !ok {
114 return nil, nil, fmt.Errorf("spec.values not found")
115 }
116 valuesM, ok := values.(map[string]any)
117 if !ok {
118 return nil, nil, fmt.Errorf("spec.values is not a map")
119 }
120 tcp, ok := valuesM["tcp"]
121 if !ok {
122 tcp = map[string]any{}
123 valuesM["tcp"] = tcp
124 }
125 udp, ok := valuesM["udp"]
126 if !ok {
127 udp = map[string]any{}
128 valuesM["udp"] = udp
129 }
130 tcpM, ok := tcp.(map[string]any)
131 if !ok {
132 return nil, nil, fmt.Errorf("spec.values.tcp is not a map")
133 }
134 udpM, ok := udp.(map[string]any)
135 if !ok {
136 return nil, nil, fmt.Errorf("spec.values.udp is not a map")
137 }
138 return tcpM, udpM, nil
139}
140
141func addPort(pm map[string]any, req allocateReq) error {
142 sourcePortStr := strconv.Itoa(req.SourcePort)
giobbc6fad2024-04-12 15:53:05 +0400143 if _, ok := pm[sourcePortStr]; ok || req.SourcePort == 80 || req.SourcePort == 443 || req.SourcePort == 22 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400144 return fmt.Errorf("port %d is already taken", req.SourcePort)
145 }
146 pm[sourcePortStr] = fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort)
147 return nil
148}
149
150func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
151 if r.Method != http.MethodPost {
152 http.Error(w, "only post method is supported", http.StatusBadRequest)
153 return
154 }
155 req, err := extractAllocateReq(r.Body)
156 if err != nil {
157 http.Error(w, err.Error(), http.StatusBadRequest)
158 return
159 }
160 fmt.Printf("%+v\n", req)
161 ingressRel, err := s.client.ReadRelease()
162 if err != nil {
163 http.Error(w, err.Error(), http.StatusInternalServerError)
164 return
165 }
166 fmt.Printf("%+v\n", ingressRel)
167 tcp, udp, err := extractPorts(ingressRel)
168 if err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
170 return
171 }
172 fmt.Printf("%+v %+v\n", tcp, udp)
173 switch req.Protocol {
174 case "tcp":
175 if err := addPort(tcp, req); err != nil {
176 http.Error(w, err.Error(), http.StatusConflict)
177 return
178 }
179 case "udp":
180 if err := addPort(udp, req); err != nil {
181 http.Error(w, err.Error(), http.StatusConflict)
182 return
183 }
184 default:
185 panic("MUST NOT REACH")
186 }
187 commitMsg := fmt.Sprintf("ingress: port map %d %s", req.SourcePort, req.Protocol)
188 if err := s.client.WriteRelease(ingressRel, commitMsg); err != nil {
189 http.Error(w, err.Error(), http.StatusInternalServerError)
190 return
191 }
192}
193
194// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400195func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400196 sshKey, err := os.ReadFile(keyPath)
197 if err != nil {
198 return nil, err
199 }
200 signer, err := ssh.ParsePrivateKey(sshKey)
201 if err != nil {
202 return nil, err
203 }
204 repoAddr, err := soft.ParseRepositoryAddress(addr)
205 if err != nil {
206 return nil, err
207 }
208 repo, err := soft.CloneRepository(repoAddr, signer)
209 if err != nil {
210 return nil, err
211 }
gioe72b54f2024-04-22 10:44:41 +0400212 return soft.NewRepoIO(repo, signer), nil
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400213}
214
215func main() {
216 flag.Parse()
217 repo, err := createRepoClient(*repoAddr, *sshKey)
218 if err != nil {
219 log.Fatal(err)
220 }
221 s := newServer(
222 *port,
223 &repoClient{repo, *ingressNginxPath},
224 )
225 log.Fatal(s.Start())
226}