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