blob: d2c5d006cc42414d455118048b9b15da179d0e72 [file] [log] [blame]
giolekva8aa73e82022-07-09 11:34:39 +04001package installer
giolekva050609f2021-12-29 15:51:40 +04002
giolekva8aa73e82022-07-09 11:34:39 +04003import (
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +04004 "bytes"
gio0eaf2712024-04-14 13:08:46 +04005 _ "embed"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +04006 "encoding/json"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +04007 "fmt"
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +04008 template "html/template"
gio3cdee592024-04-17 10:15:56 +04009 "net"
gioe72b54f2024-04-22 10:44:41 +040010 "net/netip"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040011 "strings"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040012
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040013 "cuelang.org/go/cue"
gio308105e2024-04-19 13:12:13 +040014 "cuelang.org/go/cue/build"
15 "cuelang.org/go/cue/cuecontext"
16 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040017 cueyaml "cuelang.org/go/encoding/yaml"
giof8843412024-05-22 16:38:05 +040018 helmv2 "github.com/fluxcd/helm-controller/api/v2"
giolekva8aa73e82022-07-09 11:34:39 +040019)
giolekva050609f2021-12-29 15:51:40 +040020
giof8843412024-05-22 16:38:05 +040021//go:embed app_configs/dodo_app.cue
22var dodoAppCue []byte
gio0eaf2712024-04-14 13:08:46 +040023
giof8843412024-05-22 16:38:05 +040024//go:embed app_configs/app_base.cue
25var cueBaseConfig []byte
gio0eaf2712024-04-14 13:08:46 +040026
giof8843412024-05-22 16:38:05 +040027//go:embed app_configs/app_global_env.cue
28var cueEnvAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040029
giof8843412024-05-22 16:38:05 +040030//go:embed app_configs/app_global_infra.cue
31var cueInfraAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040032
gioe72b54f2024-04-22 10:44:41 +040033type rendered struct {
giof8843412024-05-22 16:38:05 +040034 Name string
35 Readme string
36 Resources CueAppData
37 HelmCharts HelmCharts
38 ContainerImages map[string]ContainerImage
39 Ports []PortForward
40 Data CueAppData
41 URL string
42 Help []HelpDocument
43 Icon string
Davit Tabidze56f86a42024-04-09 19:15:25 +040044}
45
46type HelpDocument struct {
47 Title string
48 Contents string
49 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040050}
51
giof8843412024-05-22 16:38:05 +040052type ContainerImage struct {
53 Registry string `json:"registry"`
54 Repository string `json:"repository"`
55 Name string `json:"name"`
56 Tag string `json:"tag"`
57}
58
59type helmChartRef struct {
60 Kind string `json:"kind"`
61}
62
63type HelmCharts struct {
64 Git map[string]HelmChartGitRepo
65}
66
67type HelmChartGitRepo struct {
68 Address string `json:"address"`
69 Branch string `json:"branch"`
70 Path string `json:"path"`
71}
72
gioe72b54f2024-04-22 10:44:41 +040073type EnvAppRendered struct {
74 rendered
75 Config AppInstanceConfig
76}
77
78type InfraAppRendered struct {
79 rendered
80 Config InfraAppInstanceConfig
81}
82
gio3cdee592024-04-17 10:15:56 +040083type PortForward struct {
84 Allocator string `json:"allocator"`
gioefa0ed42024-06-13 12:31:43 +040085 ReserveAddr string `json:"reservator"`
giocdfa3722024-06-13 20:10:14 +040086 RemoveAddr string `json:"deallocator"`
gio3cdee592024-04-17 10:15:56 +040087 Protocol string `json:"protocol"`
88 SourcePort int `json:"sourcePort"`
89 TargetService string `json:"targetService"`
90 TargetPort int `json:"targetPort"`
91}
92
93type AppType int
94
95const (
96 AppTypeInfra AppType = iota
97 AppTypeEnv
98)
99
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400100type App interface {
101 Name() string
gio44f621b2024-04-29 09:44:38 +0400102 Type() AppType
103 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400104 Description() string
105 Icon() template.HTML
106 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400107 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400108}
109
110type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400111 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
112 PublicIP []net.IP `json:"publicIP,omitempty"`
113 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
114 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400115}
116
117type InfraApp interface {
118 App
giof8843412024-05-22 16:38:05 +0400119 Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400120}
121
122type EnvNetwork struct {
123 DNS net.IP `json:"dns,omitempty"`
124 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
125 Ingress net.IP `json:"ingress,omitempty"`
126 Headscale net.IP `json:"headscale,omitempty"`
127 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
128 ServicesTo net.IP `json:"servicesTo,omitempty"`
129}
130
131func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
132 addr, err := netip.ParseAddr(subnet.String())
133 if err != nil {
134 return EnvNetwork{}, err
135 }
136 if !addr.Is4() {
137 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
138 }
139 dns := addr.Next()
140 ingress := dns.Next()
141 headscale := ingress.Next()
142 b := addr.AsSlice()
143 if b[3] != 0 {
144 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
145 }
146 b[3] = 10
147 servicesFrom, ok := netip.AddrFromSlice(b)
148 if !ok {
149 return EnvNetwork{}, fmt.Errorf("Must not reach")
150 }
151 b[3] = 254
152 servicesTo, ok := netip.AddrFromSlice(b)
153 if !ok {
154 return EnvNetwork{}, fmt.Errorf("Must not reach")
155 }
156 b[3] = b[2]
157 b[2] = b[1]
158 b[0] = 10
159 b[1] = 44
160 dnsInClusterIP, ok := netip.AddrFromSlice(b)
161 if !ok {
162 return EnvNetwork{}, fmt.Errorf("Must not reach")
163 }
164 return EnvNetwork{
165 DNS: net.ParseIP(dns.String()),
166 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
167 Ingress: net.ParseIP(ingress.String()),
168 Headscale: net.ParseIP(headscale.String()),
169 ServicesFrom: net.ParseIP(servicesFrom.String()),
170 ServicesTo: net.ParseIP(servicesTo.String()),
171 }, nil
gio3cdee592024-04-17 10:15:56 +0400172}
173
gioe72b54f2024-04-22 10:44:41 +0400174type EnvConfig struct {
175 Id string `json:"id,omitempty"`
176 InfraName string `json:"pcloudEnvName,omitempty"`
177 Domain string `json:"domain,omitempty"`
178 PrivateDomain string `json:"privateDomain,omitempty"`
179 ContactEmail string `json:"contactEmail,omitempty"`
180 AdminPublicKey string `json:"adminPublicKey,omitempty"`
181 PublicIP []net.IP `json:"publicIP,omitempty"`
182 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
183 NamespacePrefix string `json:"namespacePrefix,omitempty"`
184 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400185}
186
187type EnvApp interface {
188 App
giocb34ad22024-07-11 08:01:13 +0400189 Render(release Release, env EnvConfig, networks []Network, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400190}
191
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400192type cueApp struct {
193 name string
194 description string
195 icon template.HTML
196 namespace string
197 schema Schema
gio308105e2024-04-19 13:12:13 +0400198 cfg cue.Value
199 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400200}
201
gio308105e2024-04-19 13:12:13 +0400202type CueAppData map[string][]byte
203
204func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
205 ctx := cuecontext.New()
206 buildCtx := build.NewContext()
207 cfg := &load.Config{
208 Context: buildCtx,
209 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400210 }
gio308105e2024-04-19 13:12:13 +0400211 names := make([]string, 0)
212 for n, b := range data {
213 a := fmt.Sprintf("/%s", n)
214 names = append(names, a)
215 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
216 }
217 instances := load.Instances(names, cfg)
218 for _, inst := range instances {
219 if inst.Err != nil {
220 return cue.Value{}, inst.Err
221 }
222 }
223 if len(instances) != 1 {
224 return cue.Value{}, fmt.Errorf("invalid")
225 }
226 ret := ctx.BuildInstance(instances[0])
227 if ret.Err() != nil {
228 return cue.Value{}, ret.Err()
229 }
230 if err := ret.Validate(); err != nil {
231 return cue.Value{}, err
232 }
233 return ret, nil
234}
235
236func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400237 cfg := struct {
238 Name string `json:"name"`
239 Namespace string `json:"namespace"`
240 Description string `json:"description"`
241 Icon string `json:"icon"`
242 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400243 if err := config.Decode(&cfg); err != nil {
244 return cueApp{}, err
245 }
gio44f621b2024-04-29 09:44:38 +0400246 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400247 if err != nil {
248 return cueApp{}, err
249 }
250 return cueApp{
251 name: cfg.Name,
252 description: cfg.Description,
253 icon: template.HTML(cfg.Icon),
254 namespace: cfg.Namespace,
255 schema: schema,
256 cfg: config,
gio308105e2024-04-19 13:12:13 +0400257 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400258 }, nil
259}
260
gio308105e2024-04-19 13:12:13 +0400261func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
262 config, err := ParseCueAppConfig(data)
263 if err != nil {
264 return cueApp{}, err
265 }
266 return newCueApp(config, data)
267}
268
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400269func (a cueApp) Name() string {
270 return a.name
271}
272
gio44f621b2024-04-29 09:44:38 +0400273func (a cueApp) Slug() string {
274 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
275}
276
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400277func (a cueApp) Description() string {
278 return a.description
279}
280
281func (a cueApp) Icon() template.HTML {
282 return a.icon
283}
284
285func (a cueApp) Schema() Schema {
286 return a.schema
287}
288
gioef01fbb2024-04-12 16:52:59 +0400289func (a cueApp) Namespace() string {
290 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400291}
292
gioe72b54f2024-04-22 10:44:41 +0400293func (a cueApp) render(values map[string]any) (rendered, error) {
294 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400295 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400296 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400297 HelmCharts: HelmCharts{
298 Git: make(map[string]HelmChartGitRepo),
299 },
300 ContainerImages: make(map[string]ContainerImage),
301 Ports: make([]PortForward, 0),
302 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400303 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400304 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400305 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400306 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400307 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400308 ctx := a.cfg.Context()
309 d := ctx.CompileBytes(buf.Bytes())
310 res := a.cfg.Unify(d).Eval()
311 if err := res.Err(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400312 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400313 }
314 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400315 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400316 }
gio308105e2024-04-19 13:12:13 +0400317 full, err := json.MarshalIndent(res, "", "\t")
318 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400319 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400320 }
321 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400322 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
323 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400324 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400325 }
326 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400327 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400328 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400329 }
gio0eaf2712024-04-14 13:08:46 +0400330 {
giof8843412024-05-22 16:38:05 +0400331 charts := res.LookupPath(cue.ParsePath("charts"))
332 i, err := charts.Fields()
333 if err != nil {
334 return rendered{}, err
335 }
336 for i.Next() {
337 var chartRef helmChartRef
338 if err := i.Value().Decode(&chartRef); err != nil {
339 return rendered{}, err
340 }
341 if chartRef.Kind == "GitRepository" {
342 var chart HelmChartGitRepo
343 if err := i.Value().Decode(&chart); err != nil {
344 return rendered{}, err
345 }
346 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
347 }
348 }
349 }
350 {
351 images := res.LookupPath(cue.ParsePath("images"))
352 i, err := images.Fields()
353 if err != nil {
354 return rendered{}, err
355 }
356 for i.Next() {
357 var img ContainerImage
358 if err := i.Value().Decode(&img); err != nil {
359 return rendered{}, err
360 }
361 ret.ContainerImages[cleanName(i.Selector().String())] = img
362 }
363 }
364 {
gio0eaf2712024-04-14 13:08:46 +0400365 output := res.LookupPath(cue.ParsePath("output"))
366 i, err := output.Fields()
367 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400368 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400369 }
370 for i.Next() {
371 if contents, err := cueyaml.Encode(i.Value()); err != nil {
372 return rendered{}, err
373 } else {
374 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
375 ret.Resources[name] = contents
376 }
377 }
378 }
379 {
380 resources := res.LookupPath(cue.ParsePath("resources"))
381 i, err := resources.Fields()
382 if err != nil {
383 return rendered{}, err
384 }
385 for i.Next() {
386 if contents, err := cueyaml.Encode(i.Value()); err != nil {
387 return rendered{}, err
388 } else {
389 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
390 ret.Resources[name] = contents
391 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400392 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400393 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400394 helpValue := res.LookupPath(cue.ParsePath("help"))
395 if helpValue.Exists() {
396 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400397 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400398 }
399 }
400 url, err := res.LookupPath(cue.ParsePath("url")).String()
401 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400402 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400403 }
gio09a3e5b2024-04-26 14:11:06 +0400404 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400405 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
406 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400407 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400408 }
409 ret.Icon = icon
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400410 return ret, nil
411}
412
gio3cdee592024-04-17 10:15:56 +0400413type cueEnvApp struct {
414 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400415}
416
gio308105e2024-04-19 13:12:13 +0400417func NewCueEnvApp(data CueAppData) (EnvApp, error) {
418 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400419 if err != nil {
420 return nil, err
421 }
gio3cdee592024-04-17 10:15:56 +0400422 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400423}
424
gio0eaf2712024-04-14 13:08:46 +0400425func NewDodoApp(appCfg []byte) (EnvApp, error) {
426 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400427 "app.cue": appCfg,
428 "base.cue": []byte(cueBaseConfig),
429 "dodo.cue": dodoAppCue,
430 "env.cue": []byte(cueEnvAppGlobal),
gio0eaf2712024-04-14 13:08:46 +0400431 })
432}
433
gio3cdee592024-04-17 10:15:56 +0400434func (a cueEnvApp) Type() AppType {
435 return AppTypeEnv
436}
437
giocb34ad22024-07-11 08:01:13 +0400438func (a cueEnvApp) Render(
439 release Release,
440 env EnvConfig,
441 networks []Network,
442 values map[string]any,
443 charts map[string]helmv2.HelmChartTemplateSpec,
444) (EnvAppRendered, error) {
gio3cdee592024-04-17 10:15:56 +0400445 derived, err := deriveValues(values, a.Schema(), networks)
446 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400447 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400448 }
giof8843412024-05-22 16:38:05 +0400449 if charts == nil {
450 charts = make(map[string]helmv2.HelmChartTemplateSpec)
451 }
gio3cdee592024-04-17 10:15:56 +0400452 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400453 "global": env,
454 "release": release,
455 "input": derived,
456 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400457 "networks": NetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400458 })
459 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400460 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400461 }
gioe72b54f2024-04-22 10:44:41 +0400462 return EnvAppRendered{
463 rendered: ret,
464 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400465 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400466 Env: env,
467 Release: release,
468 Values: values,
469 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400470 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400471 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400472 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400473 },
474 }, nil
gio3cdee592024-04-17 10:15:56 +0400475}
476
477type cueInfraApp struct {
478 cueApp
479}
480
gio308105e2024-04-19 13:12:13 +0400481func NewCueInfraApp(data CueAppData) (InfraApp, error) {
482 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400483 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400484 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400485 }
gio3cdee592024-04-17 10:15:56 +0400486 return cueInfraApp{app}, nil
487}
488
489func (a cueInfraApp) Type() AppType {
490 return AppTypeInfra
491}
492
giof8843412024-05-22 16:38:05 +0400493func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
494 if charts == nil {
495 charts = make(map[string]helmv2.HelmChartTemplateSpec)
496 }
gioe72b54f2024-04-22 10:44:41 +0400497 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400498 "global": infra,
499 "release": release,
500 "input": values,
501 "localCharts": charts,
gio3cdee592024-04-17 10:15:56 +0400502 })
gioe72b54f2024-04-22 10:44:41 +0400503 if err != nil {
504 return InfraAppRendered{}, err
505 }
506 return InfraAppRendered{
507 rendered: ret,
508 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400509 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400510 Infra: infra,
511 Release: release,
512 Values: values,
513 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400514 URL: ret.URL,
515 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400516 },
517 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400518}
519
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400520func cleanName(s string) string {
521 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400522}
gioe72b54f2024-04-22 10:44:41 +0400523
524func join[T fmt.Stringer](items []T, sep string) string {
525 var tmp []string
526 for _, i := range items {
527 tmp = append(tmp, i.String())
528 }
529 return strings.Join(tmp, ",")
530}
gio0eaf2712024-04-14 13:08:46 +0400531
gio5e49bb62024-07-20 10:43:19 +0400532func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400533 ret := make(map[string]Network)
534 for _, n := range networks {
535 ret[strings.ToLower(n.Name)] = n
536 }
537 return ret
538}