blob: fb5a1513aa4be07a750b9064a7b1a46ecc7f99fe [file] [log] [blame]
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04001package main
2
3import (
gioefa0ed42024-06-13 12:31:43 +04004 "crypto/rand"
giob1c4e542024-07-15 12:10:52 +04005 "encoding/base64"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04006 "encoding/json"
7 "flag"
8 "fmt"
9 "io"
10 "log"
gioefa0ed42024-06-13 12:31:43 +040011 "math/big"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040012 "net/http"
13 "os"
14 "strconv"
15 "strings"
gioefa0ed42024-06-13 12:31:43 +040016 "sync"
17 "time"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040018
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040019 "github.com/giolekva/pcloud/core/installer/soft"
20
21 "golang.org/x/crypto/ssh"
22)
23
Davit Tabidze6bf29832024-06-17 16:51:54 +040024const (
25 secretLength = 20
26)
27
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040028var port = flag.Int("port", 8080, "Port to listen on")
29var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
30var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
31var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
giod28f83c2024-08-15 10:53:40 +040032var minPreOpenPorts = flag.Int("min-pre-open-ports", 5, "Minimum number of pre-open ports to keep in reserve")
33var preOpenPortsBatchSize = flag.Int("pre-open-ports-batch-size", 10, "Number of new ports to open at a time")
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040034
35type client interface {
giod28f83c2024-08-15 10:53:40 +040036 ReservePort() (int, string, error)
37 ReleaseReservedPort(port int)
38 AddPortForwarding(protocol string, port int, secret, dest string) error
39 RemovePortForwarding(protocol string, port int) error
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040040}
41
42type repoClient struct {
giod28f83c2024-08-15 10:53:40 +040043 l sync.Locker
44 repo soft.RepoIO
45 path string
46 minPreOpenPorts int
47 preOpenPortsBatchSize int
48 preOpenPorts []int
49 blocklist map[int]struct{}
50 reserve map[int]string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040051}
52
giod28f83c2024-08-15 10:53:40 +040053func newRepoClient(
54 repo soft.RepoIO,
55 path string,
56 minPreOpenPorts int,
57 preOpenPortsBatchSize int,
58) (client, error) {
59 ret := &repoClient{
60 l: &sync.Mutex{},
61 repo: repo,
62 path: path,
63 minPreOpenPorts: minPreOpenPorts,
64 preOpenPortsBatchSize: preOpenPortsBatchSize,
65 }
66 r, err := repo.Reader(fmt.Sprintf("%s-state.json", path))
67 if err != nil {
68 // TODO(gio): create empty file on init
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040069 return nil, err
70 }
giod28f83c2024-08-15 10:53:40 +040071 defer r.Close()
72 var st state
73 if err := json.NewDecoder(r).Decode(&st); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040074 return nil, err
75 }
giod28f83c2024-08-15 10:53:40 +040076 ret.preOpenPorts = st.PreOpenPorts
77 ret.blocklist = st.Blocklist
78 ret.reserve = map[int]string{}
79 if len(ret.preOpenPorts) < minPreOpenPorts {
80 if err := ret.preOpenNewPorts(); err != nil {
81 return nil, err
82 }
83 }
84 return ret, nil
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040085}
86
giod28f83c2024-08-15 10:53:40 +040087func (c *repoClient) ReservePort() (int, string, error) {
88 c.l.Lock()
89 defer c.l.Unlock()
90 if len(c.preOpenPorts) == 0 {
91 return -1, "", fmt.Errorf("no pre-open ports are available")
92 }
93 port := c.preOpenPorts[0]
94 c.preOpenPorts = c.preOpenPorts[1:]
95 secret, err := generateSecret()
96 if err != nil {
97 return -1, "", err
98 }
99 c.reserve[port] = secret
100 return port, secret, nil
101}
102
103func (c *repoClient) ReleaseReservedPort(port int) {
104 c.l.Lock()
105 defer c.l.Unlock()
106 delete(c.reserve, port)
107 c.preOpenPorts = append(c.preOpenPorts, port)
108}
109
110type state struct {
111 PreOpenPorts []int `json:"preOpenPorts"`
112 Blocklist map[int]struct{} `json:"blocklist"`
113}
114
115func (c *repoClient) preOpenNewPorts() error {
116 c.l.Lock()
117 defer c.l.Unlock()
118 if len(c.preOpenPorts) >= c.minPreOpenPorts {
119 return nil
120 }
121 var ports []int
122 for count := c.preOpenPortsBatchSize; count > 0; count-- {
123 generated := false
124 for i := 0; i < 3; i++ {
125 r, err := rand.Int(rand.Reader, big.NewInt(end-start))
126 if err != nil {
127 return err
128 }
129 p := start + int(r.Int64())
130 if _, ok := c.blocklist[p]; !ok {
131 generated = true
132 ports = append(ports, p)
133 c.preOpenPorts = append(c.preOpenPorts, p)
134 c.blocklist[p] = struct{}{}
135 break
136 }
137 }
138 if !generated {
139 return fmt.Errorf("could not open new port")
140 }
141 }
142 return c.repo.Do(func(fs soft.RepoFS) (string, error) {
143 if err := c.writeState(fs); err != nil {
144 return "", err
145 }
146 rel, err := c.readRelease(fs)
147 if err != nil {
148 return "", err
149 }
gioa344a2a2024-08-16 17:13:48 +0400150 svcType, err := extractString(rel, "spec.values.controller.service.type")
giod28f83c2024-08-15 10:53:40 +0400151 if err != nil {
152 return "", err
153 }
gioa344a2a2024-08-16 17:13:48 +0400154 if svcType == "NodePort" {
155 tcp, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
156 if err != nil {
157 return "", err
158 }
159 udp, err := extractPorts(rel, "spec.values.controller.service.nodePorts.udp")
160 if err != nil {
161 return "", err
162 }
163 for _, p := range ports {
164 ps := strconv.Itoa(p)
165 tcp[ps] = p
166 udp[ps] = p
167 }
168 if err := c.writeRelease(fs, rel); err != nil {
169 return "", err
170 }
giod28f83c2024-08-15 10:53:40 +0400171 }
gioa344a2a2024-08-16 17:13:48 +0400172 fmt.Printf("Pre opened new ports: %+v\n", ports)
giod28f83c2024-08-15 10:53:40 +0400173 return "preopen new ports", nil
174 })
175}
176
177func (c *repoClient) AddPortForwarding(protocol string, port int, secret, dest string) error {
178 defer func() {
179 go func() {
180 if err := c.preOpenNewPorts(); err != nil {
181 panic(err)
182 }
183 }()
184 }()
185 c.l.Lock()
186 defer c.l.Unlock()
187 if sec, ok := c.reserve[port]; !ok || sec != secret {
188 return fmt.Errorf("wrong secret")
189 }
190 delete(c.reserve, port)
191 return c.repo.Do(func(fs soft.RepoFS) (string, error) {
192 if err := c.writeState(fs); err != nil {
193 return "", err
194 }
195 rel, err := c.readRelease(fs)
196 if err != nil {
197 return "", err
198 }
199 portStr := strconv.Itoa(port)
200 switch protocol {
201 case "tcp":
202 tcp, err := extractPorts(rel, "spec.values.tcp")
203 if err != nil {
204 return "", err
205 }
206 tcp[portStr] = dest
207 case "udp":
208 udp, err := extractPorts(rel, "spec.values.udp")
209 if err != nil {
210 return "", err
211 }
212 udp[portStr] = dest
213 default:
214 panic("MUST NOT REACH")
215 }
216 if err := c.writeRelease(fs, rel); err != nil {
217 return "", err
218 }
219 return fmt.Sprintf("ingress: port %s map %d %s", protocol, port, dest), nil
220 })
221}
222
223func (c *repoClient) RemovePortForwarding(protocol string, port int) error {
224 c.l.Lock()
225 defer c.l.Unlock()
226 return c.repo.Do(func(fs soft.RepoFS) (string, error) {
227 rel, err := c.readRelease(fs)
228 if err != nil {
229 return "", err
230 }
231 switch protocol {
232 case "tcp":
233 tcp, err := extractPorts(rel, "spec.values.tcp")
234 if err != nil {
235 return "", err
236 }
237 if err := removePort(tcp, port); err != nil {
238 return "", err
239 }
240 case "udp":
241 udp, err := extractPorts(rel, "spec.values.udp")
242 if err != nil {
243 return "", err
244 }
245 if err := removePort(udp, port); err != nil {
246 return "", err
247 }
248 default:
249 panic("MUST NOT REACH")
250 }
gioa344a2a2024-08-16 17:13:48 +0400251 svcType, err := extractString(rel, "spec.values.controller.service.type")
giod28f83c2024-08-15 10:53:40 +0400252 if err != nil {
253 return "", err
254 }
gioa344a2a2024-08-16 17:13:48 +0400255 if svcType == "NodePort" {
256 svcTCP, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
257 if err != nil {
258 return "", err
259 }
260 svcUDP, err := extractPorts(rel, "spec.values.controller.service.nodePorts.udp")
261 if err != nil {
262 return "", err
263 }
264 if err := removePort(svcTCP, port); err != nil {
265 return "", err
266 }
267 if err := removePort(svcUDP, port); err != nil {
268 return "", err
269 }
giod28f83c2024-08-15 10:53:40 +0400270 }
271 if err := c.writeRelease(fs, rel); err != nil {
272 return "", err
273 }
274 return fmt.Sprintf("ingress: remove %s port map %d", protocol, port), nil
275 })
276}
277
278func (c *repoClient) writeState(fs soft.RepoFS) error {
279 w, err := fs.Writer(fmt.Sprintf("%s-state.json", c.path))
280 if err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400281 return err
282 }
giod28f83c2024-08-15 10:53:40 +0400283 defer w.Close()
284 if err := json.NewEncoder(w).Encode(state{c.preOpenPorts, c.blocklist}); err != nil {
285 return err
286 }
287 return err
288}
289
290func (c *repoClient) readRelease(fs soft.RepoFS) (map[string]any, error) {
291 ret := map[string]any{}
292 if err := soft.ReadYaml(fs, c.path, &ret); err != nil {
293 return nil, err
294 }
295 return ret, nil
296}
297
298func (c *repoClient) writeRelease(fs soft.RepoFS, rel map[string]any) error {
299 return soft.WriteYaml(fs, c.path, rel)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400300}
301
302type server struct {
giod28f83c2024-08-15 10:53:40 +0400303 s *http.Server
304 r *http.ServeMux
305 client client
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400306}
307
308func newServer(port int, client client) *server {
309 r := http.NewServeMux()
310 s := &http.Server{
311 Addr: fmt.Sprintf(":%d", port),
312 Handler: r,
313 }
giod28f83c2024-08-15 10:53:40 +0400314 return &server{s, r, client}
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400315}
316
317func (s *server) Start() error {
gioefa0ed42024-06-13 12:31:43 +0400318 s.r.HandleFunc("/api/reserve", s.handleReserve)
giocdfa3722024-06-13 20:10:14 +0400319 s.r.HandleFunc("/api/allocate", s.handleAllocate)
320 s.r.HandleFunc("/api/remove", s.handleRemove)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400321 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
322 return err
323 }
324 return nil
325}
326
327func (s *server) Close() error {
328 return s.s.Close()
329}
330
331type allocateReq struct {
332 Protocol string `json:"protocol"`
333 SourcePort int `json:"sourcePort"`
334 TargetService string `json:"targetService"`
335 TargetPort int `json:"targetPort"`
giobd7ab0b2024-06-17 12:55:17 +0400336 Secret string `json:"secret"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400337}
338
giocdfa3722024-06-13 20:10:14 +0400339type removeReq struct {
340 Protocol string `json:"protocol"`
341 SourcePort int `json:"sourcePort"`
342 TargetService string `json:"targetService"`
343 TargetPort int `json:"targetPort"`
344}
345
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400346func extractAllocateReq(r io.Reader) (allocateReq, error) {
347 var req allocateReq
348 if err := json.NewDecoder(r).Decode(&req); err != nil {
349 return allocateReq{}, err
350 }
351 req.Protocol = strings.ToLower(req.Protocol)
352 if req.Protocol != "tcp" && req.Protocol != "udp" {
353 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
354 }
355 return req, nil
356}
357
giocdfa3722024-06-13 20:10:14 +0400358func extractRemoveReq(r io.Reader) (removeReq, error) {
359 var req removeReq
360 if err := json.NewDecoder(r).Decode(&req); err != nil {
361 return removeReq{}, err
362 }
363 req.Protocol = strings.ToLower(req.Protocol)
364 if req.Protocol != "tcp" && req.Protocol != "udp" {
365 return removeReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
366 }
367 return req, nil
368}
369
gioefa0ed42024-06-13 12:31:43 +0400370type reserveResp struct {
371 Port int `json:"port"`
372 Secret string `json:"secret"`
373}
374
gioa344a2a2024-08-16 17:13:48 +0400375func extractField(data map[string]any, path string) (any, error) {
376 var val any = data
giod28f83c2024-08-15 10:53:40 +0400377 for _, i := range strings.Split(path, ".") {
gioa344a2a2024-08-16 17:13:48 +0400378 valM, ok := val.(map[string]any)
379 if !ok {
380 return nil, fmt.Errorf("expected map")
381 }
382 val, ok = valM[i]
giod28f83c2024-08-15 10:53:40 +0400383 if !ok {
384 return nil, fmt.Errorf("%s not found", i)
385 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400386 }
gioa344a2a2024-08-16 17:13:48 +0400387 return val, nil
388}
389
390func extractPorts(data map[string]any, path string) (map[string]any, error) {
391 ret, err := extractField(data, path)
392 if err != nil {
393 return nil, err
394 }
395 retM, ok := ret.(map[string]any)
396 if !ok {
397 return nil, fmt.Errorf("expected map")
398 }
399 return retM, nil
400}
401
402func extractString(data map[string]any, path string) (string, error) {
403 ret, err := extractField(data, path)
404 if err != nil {
405 return "", err
406 }
407 retS, ok := ret.(string)
408 if !ok {
409 return "", fmt.Errorf("expected map")
410 }
411 return retS, nil
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400412}
413
giod28f83c2024-08-15 10:53:40 +0400414func addPort(pm map[string]any, sourcePort int, targetService string, targetPort int) error {
415 sourcePortStr := strconv.Itoa(sourcePort)
416 if _, ok := pm[sourcePortStr]; ok || sourcePort == 80 || sourcePort == 443 || sourcePort == 22 {
417 return fmt.Errorf("port %d is already taken", sourcePort)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400418 }
giod28f83c2024-08-15 10:53:40 +0400419 pm[sourcePortStr] = fmt.Sprintf("%s:%d", targetService, targetPort)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400420 return nil
421}
422
giod28f83c2024-08-15 10:53:40 +0400423func removePort(pm map[string]any, port int) error {
424 sourcePortStr := strconv.Itoa(port)
giocdfa3722024-06-13 20:10:14 +0400425 if _, ok := pm[sourcePortStr]; !ok {
giod28f83c2024-08-15 10:53:40 +0400426 return fmt.Errorf("port %d is not open to remove", port)
giocdfa3722024-06-13 20:10:14 +0400427 }
428 delete(pm, sourcePortStr)
429 return nil
430}
431
gioefa0ed42024-06-13 12:31:43 +0400432const start = 49152
433const end = 65535
434
giod28f83c2024-08-15 10:53:40 +0400435func updateNodePorts(rel map[string]any, protocol string, pm map[string]any) error {
436 spec, ok := rel["spec"]
437 if !ok {
438 return fmt.Errorf("spec not found")
439 }
440 specM, ok := spec.(map[string]any)
441 if !ok {
442 return fmt.Errorf("spec is not a map")
443 }
444 values, ok := specM["values"]
445 if !ok {
446 return fmt.Errorf("spec.values not found")
447 }
448 valuesM, ok := values.(map[string]any)
449 if !ok {
450 return fmt.Errorf("spec.values is not a map")
451 }
452 controller, ok := valuesM["controller"]
453 if !ok {
454 return fmt.Errorf("spec.values.controller not found")
455 }
456 controllerM, ok := controller.(map[string]any)
457 if !ok {
458 return fmt.Errorf("spec.values.controller is not a map")
459 }
460 service, ok := controllerM["service"]
461 if !ok {
462 return fmt.Errorf("spec.values.controller.service not found")
463 }
464 serviceM, ok := service.(map[string]any)
465 if !ok {
466 return fmt.Errorf("spec.values.controller.service is not a map")
467 }
468 nodePorts, ok := serviceM["nodePorts"]
469 if !ok {
470 return fmt.Errorf("spec.values.controller.service.nodePorts not found")
471 }
472 nodePortsM, ok := nodePorts.(map[string]any)
473 if !ok {
474 return fmt.Errorf("spec.values.controller.service.nodePorts is not a map")
475 }
476 npm := map[string]any{}
477 for p, _ := range pm {
478 if v, err := strconv.Atoi(p); err != nil {
479 return err
480 } else {
481 npm[p] = v
gioefa0ed42024-06-13 12:31:43 +0400482 }
483 }
giod28f83c2024-08-15 10:53:40 +0400484 nodePortsM[protocol] = npm
485 return nil
gioefa0ed42024-06-13 12:31:43 +0400486}
487
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400488func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
489 if r.Method != http.MethodPost {
490 http.Error(w, "only post method is supported", http.StatusBadRequest)
491 return
492 }
493 req, err := extractAllocateReq(r.Body)
494 if err != nil {
giod28f83c2024-08-15 10:53:40 +0400495 fmt.Println(err.Error())
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400496 http.Error(w, err.Error(), http.StatusBadRequest)
497 return
498 }
giod28f83c2024-08-15 10:53:40 +0400499 if err := s.client.AddPortForwarding(
500 req.Protocol,
501 req.SourcePort,
502 req.Secret,
503 fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort),
504 ); err != nil {
505 fmt.Println(err.Error())
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400506 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400507 }
508}
509
gioefa0ed42024-06-13 12:31:43 +0400510func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
511 if r.Method != http.MethodPost {
512 http.Error(w, "only post method is supported", http.StatusBadRequest)
513 return
514 }
gioefa0ed42024-06-13 12:31:43 +0400515 var port int
giod28f83c2024-08-15 10:53:40 +0400516 var secret string
517 var err error
518 if port, secret, err = s.client.ReservePort(); err != nil {
519 fmt.Println(err.Error())
gioefa0ed42024-06-13 12:31:43 +0400520 http.Error(w, err.Error(), http.StatusInternalServerError)
521 return
522 }
gioefa0ed42024-06-13 12:31:43 +0400523 go func() {
524 time.Sleep(30 * time.Minute)
giod28f83c2024-08-15 10:53:40 +0400525 s.client.ReleaseReservedPort(port)
gioefa0ed42024-06-13 12:31:43 +0400526 }()
giod28f83c2024-08-15 10:53:40 +0400527 if err := json.NewEncoder(w).Encode(reserveResp{port, secret}); err != nil {
528 fmt.Println(err.Error())
gioefa0ed42024-06-13 12:31:43 +0400529 http.Error(w, err.Error(), http.StatusInternalServerError)
530 return
531 }
532}
533
giocdfa3722024-06-13 20:10:14 +0400534func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
535 if r.Method != http.MethodPost {
536 http.Error(w, "only post method is supported", http.StatusBadRequest)
537 return
538 }
539 req, err := extractRemoveReq(r.Body)
540 if err != nil {
giod28f83c2024-08-15 10:53:40 +0400541 fmt.Println(err.Error())
giocdfa3722024-06-13 20:10:14 +0400542 http.Error(w, err.Error(), http.StatusBadRequest)
543 return
544 }
giod28f83c2024-08-15 10:53:40 +0400545 if err := s.client.RemovePortForwarding(req.Protocol, req.SourcePort); err != nil {
546 fmt.Println(err.Error())
giocdfa3722024-06-13 20:10:14 +0400547 http.Error(w, err.Error(), http.StatusInternalServerError)
548 return
549 }
giocdfa3722024-06-13 20:10:14 +0400550}
551
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400552// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400553func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400554 sshKey, err := os.ReadFile(keyPath)
555 if err != nil {
556 return nil, err
557 }
558 signer, err := ssh.ParsePrivateKey(sshKey)
559 if err != nil {
560 return nil, err
561 }
562 repoAddr, err := soft.ParseRepositoryAddress(addr)
563 if err != nil {
564 return nil, err
565 }
566 repo, err := soft.CloneRepository(repoAddr, signer)
567 if err != nil {
568 return nil, err
569 }
gioff2a29a2024-05-01 17:06:42 +0400570 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400571}
572
Davit Tabidze6bf29832024-06-17 16:51:54 +0400573func generateSecret() (string, error) {
574 b := make([]byte, secretLength)
575 _, err := rand.Read(b)
576 if err != nil {
577 return "", fmt.Errorf("error generating secret: %v", err)
578 }
giob1c4e542024-07-15 12:10:52 +0400579 return base64.StdEncoding.EncodeToString(b), nil
gioefa0ed42024-06-13 12:31:43 +0400580}
581
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400582func main() {
583 flag.Parse()
584 repo, err := createRepoClient(*repoAddr, *sshKey)
585 if err != nil {
586 log.Fatal(err)
587 }
giod28f83c2024-08-15 10:53:40 +0400588 c, err := newRepoClient(
589 repo,
590 *ingressNginxPath,
591 *minPreOpenPorts,
592 *preOpenPortsBatchSize,
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400593 )
giod28f83c2024-08-15 10:53:40 +0400594 if err != nil {
595 log.Fatal(err)
596 }
597 s := newServer(*port, c)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400598 log.Fatal(s.Start())
599}