blob: e443948fbdd35dc2110c13eba2932736dd422993 [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"`
gio3cdee592024-04-17 10:15:56 +040086 Protocol string `json:"protocol"`
87 SourcePort int `json:"sourcePort"`
88 TargetService string `json:"targetService"`
89 TargetPort int `json:"targetPort"`
90}
91
92type AppType int
93
94const (
95 AppTypeInfra AppType = iota
96 AppTypeEnv
97)
98
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040099type App interface {
100 Name() string
gio44f621b2024-04-29 09:44:38 +0400101 Type() AppType
102 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400103 Description() string
104 Icon() template.HTML
105 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400106 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400107}
108
109type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400110 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
111 PublicIP []net.IP `json:"publicIP,omitempty"`
112 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
113 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400114}
115
116type InfraApp interface {
117 App
giof8843412024-05-22 16:38:05 +0400118 Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400119}
120
121type EnvNetwork struct {
122 DNS net.IP `json:"dns,omitempty"`
123 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
124 Ingress net.IP `json:"ingress,omitempty"`
125 Headscale net.IP `json:"headscale,omitempty"`
126 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
127 ServicesTo net.IP `json:"servicesTo,omitempty"`
128}
129
130func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
131 addr, err := netip.ParseAddr(subnet.String())
132 if err != nil {
133 return EnvNetwork{}, err
134 }
135 if !addr.Is4() {
136 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
137 }
138 dns := addr.Next()
139 ingress := dns.Next()
140 headscale := ingress.Next()
141 b := addr.AsSlice()
142 if b[3] != 0 {
143 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
144 }
145 b[3] = 10
146 servicesFrom, ok := netip.AddrFromSlice(b)
147 if !ok {
148 return EnvNetwork{}, fmt.Errorf("Must not reach")
149 }
150 b[3] = 254
151 servicesTo, ok := netip.AddrFromSlice(b)
152 if !ok {
153 return EnvNetwork{}, fmt.Errorf("Must not reach")
154 }
155 b[3] = b[2]
156 b[2] = b[1]
157 b[0] = 10
158 b[1] = 44
159 dnsInClusterIP, ok := netip.AddrFromSlice(b)
160 if !ok {
161 return EnvNetwork{}, fmt.Errorf("Must not reach")
162 }
163 return EnvNetwork{
164 DNS: net.ParseIP(dns.String()),
165 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
166 Ingress: net.ParseIP(ingress.String()),
167 Headscale: net.ParseIP(headscale.String()),
168 ServicesFrom: net.ParseIP(servicesFrom.String()),
169 ServicesTo: net.ParseIP(servicesTo.String()),
170 }, nil
gio3cdee592024-04-17 10:15:56 +0400171}
172
gioe72b54f2024-04-22 10:44:41 +0400173type EnvConfig struct {
174 Id string `json:"id,omitempty"`
175 InfraName string `json:"pcloudEnvName,omitempty"`
176 Domain string `json:"domain,omitempty"`
177 PrivateDomain string `json:"privateDomain,omitempty"`
178 ContactEmail string `json:"contactEmail,omitempty"`
179 AdminPublicKey string `json:"adminPublicKey,omitempty"`
180 PublicIP []net.IP `json:"publicIP,omitempty"`
181 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
182 NamespacePrefix string `json:"namespacePrefix,omitempty"`
183 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400184}
185
186type EnvApp interface {
187 App
giof8843412024-05-22 16:38:05 +0400188 Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400189}
190
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400191type cueApp struct {
192 name string
193 description string
194 icon template.HTML
195 namespace string
196 schema Schema
gio308105e2024-04-19 13:12:13 +0400197 cfg cue.Value
198 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400199}
200
gio308105e2024-04-19 13:12:13 +0400201type CueAppData map[string][]byte
202
203func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
204 ctx := cuecontext.New()
205 buildCtx := build.NewContext()
206 cfg := &load.Config{
207 Context: buildCtx,
208 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400209 }
gio308105e2024-04-19 13:12:13 +0400210 names := make([]string, 0)
211 for n, b := range data {
212 a := fmt.Sprintf("/%s", n)
213 names = append(names, a)
214 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
215 }
216 instances := load.Instances(names, cfg)
217 for _, inst := range instances {
218 if inst.Err != nil {
219 return cue.Value{}, inst.Err
220 }
221 }
222 if len(instances) != 1 {
223 return cue.Value{}, fmt.Errorf("invalid")
224 }
225 ret := ctx.BuildInstance(instances[0])
226 if ret.Err() != nil {
227 return cue.Value{}, ret.Err()
228 }
229 if err := ret.Validate(); err != nil {
230 return cue.Value{}, err
231 }
232 return ret, nil
233}
234
235func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400236 cfg := struct {
237 Name string `json:"name"`
238 Namespace string `json:"namespace"`
239 Description string `json:"description"`
240 Icon string `json:"icon"`
241 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400242 if err := config.Decode(&cfg); err != nil {
243 return cueApp{}, err
244 }
gio44f621b2024-04-29 09:44:38 +0400245 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400246 if err != nil {
247 return cueApp{}, err
248 }
249 return cueApp{
250 name: cfg.Name,
251 description: cfg.Description,
252 icon: template.HTML(cfg.Icon),
253 namespace: cfg.Namespace,
254 schema: schema,
255 cfg: config,
gio308105e2024-04-19 13:12:13 +0400256 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400257 }, nil
258}
259
gio308105e2024-04-19 13:12:13 +0400260func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
261 config, err := ParseCueAppConfig(data)
262 if err != nil {
263 return cueApp{}, err
264 }
265 return newCueApp(config, data)
266}
267
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400268func (a cueApp) Name() string {
269 return a.name
270}
271
gio44f621b2024-04-29 09:44:38 +0400272func (a cueApp) Slug() string {
273 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
274}
275
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400276func (a cueApp) Description() string {
277 return a.description
278}
279
280func (a cueApp) Icon() template.HTML {
281 return a.icon
282}
283
284func (a cueApp) Schema() Schema {
285 return a.schema
286}
287
gioef01fbb2024-04-12 16:52:59 +0400288func (a cueApp) Namespace() string {
289 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400290}
291
gioe72b54f2024-04-22 10:44:41 +0400292func (a cueApp) render(values map[string]any) (rendered, error) {
293 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400294 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400295 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400296 HelmCharts: HelmCharts{
297 Git: make(map[string]HelmChartGitRepo),
298 },
299 ContainerImages: make(map[string]ContainerImage),
300 Ports: make([]PortForward, 0),
301 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400302 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400303 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400304 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400305 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400306 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400307 ctx := a.cfg.Context()
308 d := ctx.CompileBytes(buf.Bytes())
309 res := a.cfg.Unify(d).Eval()
310 if err := res.Err(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400311 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400312 }
313 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400314 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400315 }
gio308105e2024-04-19 13:12:13 +0400316 full, err := json.MarshalIndent(res, "", "\t")
317 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400318 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400319 }
320 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400321 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
322 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400323 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400324 }
325 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400326 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400327 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400328 }
gio0eaf2712024-04-14 13:08:46 +0400329 {
giof8843412024-05-22 16:38:05 +0400330 charts := res.LookupPath(cue.ParsePath("charts"))
331 i, err := charts.Fields()
332 if err != nil {
333 return rendered{}, err
334 }
335 for i.Next() {
336 var chartRef helmChartRef
337 if err := i.Value().Decode(&chartRef); err != nil {
338 return rendered{}, err
339 }
340 if chartRef.Kind == "GitRepository" {
341 var chart HelmChartGitRepo
342 if err := i.Value().Decode(&chart); err != nil {
343 return rendered{}, err
344 }
345 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
346 }
347 }
348 }
349 {
350 images := res.LookupPath(cue.ParsePath("images"))
351 i, err := images.Fields()
352 if err != nil {
353 return rendered{}, err
354 }
355 for i.Next() {
356 var img ContainerImage
357 if err := i.Value().Decode(&img); err != nil {
358 return rendered{}, err
359 }
360 ret.ContainerImages[cleanName(i.Selector().String())] = img
361 }
362 }
363 {
gio0eaf2712024-04-14 13:08:46 +0400364 output := res.LookupPath(cue.ParsePath("output"))
365 i, err := output.Fields()
366 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400367 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400368 }
369 for i.Next() {
370 if contents, err := cueyaml.Encode(i.Value()); err != nil {
371 return rendered{}, err
372 } else {
373 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
374 ret.Resources[name] = contents
375 }
376 }
377 }
378 {
379 resources := res.LookupPath(cue.ParsePath("resources"))
380 i, err := resources.Fields()
381 if err != nil {
382 return rendered{}, err
383 }
384 for i.Next() {
385 if contents, err := cueyaml.Encode(i.Value()); err != nil {
386 return rendered{}, err
387 } else {
388 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
389 ret.Resources[name] = contents
390 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400391 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400392 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400393 helpValue := res.LookupPath(cue.ParsePath("help"))
394 if helpValue.Exists() {
395 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400396 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400397 }
398 }
399 url, err := res.LookupPath(cue.ParsePath("url")).String()
400 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400401 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400402 }
gio09a3e5b2024-04-26 14:11:06 +0400403 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400404 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
405 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400406 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400407 }
408 ret.Icon = icon
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400409 return ret, nil
410}
411
gio3cdee592024-04-17 10:15:56 +0400412type cueEnvApp struct {
413 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400414}
415
gio308105e2024-04-19 13:12:13 +0400416func NewCueEnvApp(data CueAppData) (EnvApp, error) {
417 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400418 if err != nil {
419 return nil, err
420 }
gio3cdee592024-04-17 10:15:56 +0400421 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400422}
423
gio0eaf2712024-04-14 13:08:46 +0400424func NewDodoApp(appCfg []byte) (EnvApp, error) {
425 return NewCueEnvApp(CueAppData{
426 "app.cue": appCfg,
427 "base.cue": []byte(cueBaseConfig),
giof8843412024-05-22 16:38:05 +0400428 "pcloud_app.cue": dodoAppCue,
gio0eaf2712024-04-14 13:08:46 +0400429 "env_app.cue": []byte(cueEnvAppGlobal),
430 })
431}
432
gio3cdee592024-04-17 10:15:56 +0400433func (a cueEnvApp) Type() AppType {
434 return AppTypeEnv
435}
436
giof8843412024-05-22 16:38:05 +0400437func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error) {
gio3cdee592024-04-17 10:15:56 +0400438 networks := CreateNetworks(env)
439 derived, err := deriveValues(values, a.Schema(), networks)
440 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400441 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400442 }
giof8843412024-05-22 16:38:05 +0400443 if charts == nil {
444 charts = make(map[string]helmv2.HelmChartTemplateSpec)
445 }
gio3cdee592024-04-17 10:15:56 +0400446 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400447 "global": env,
448 "release": release,
449 "input": derived,
450 "localCharts": charts,
451 "networks": networkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400452 })
453 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400454 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400455 }
gioe72b54f2024-04-22 10:44:41 +0400456 return EnvAppRendered{
457 rendered: ret,
458 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400459 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400460 Env: env,
461 Release: release,
462 Values: values,
463 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400464 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400465 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400466 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400467 },
468 }, nil
gio3cdee592024-04-17 10:15:56 +0400469}
470
471type cueInfraApp struct {
472 cueApp
473}
474
gio308105e2024-04-19 13:12:13 +0400475func NewCueInfraApp(data CueAppData) (InfraApp, error) {
476 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400477 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400478 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400479 }
gio3cdee592024-04-17 10:15:56 +0400480 return cueInfraApp{app}, nil
481}
482
483func (a cueInfraApp) Type() AppType {
484 return AppTypeInfra
485}
486
giof8843412024-05-22 16:38:05 +0400487func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
488 if charts == nil {
489 charts = make(map[string]helmv2.HelmChartTemplateSpec)
490 }
gioe72b54f2024-04-22 10:44:41 +0400491 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400492 "global": infra,
493 "release": release,
494 "input": values,
495 "localCharts": charts,
gio3cdee592024-04-17 10:15:56 +0400496 })
gioe72b54f2024-04-22 10:44:41 +0400497 if err != nil {
498 return InfraAppRendered{}, err
499 }
500 return InfraAppRendered{
501 rendered: ret,
502 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400503 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400504 Infra: infra,
505 Release: release,
506 Values: values,
507 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400508 URL: ret.URL,
509 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400510 },
511 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400512}
513
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400514func cleanName(s string) string {
515 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400516}
gioe72b54f2024-04-22 10:44:41 +0400517
518func join[T fmt.Stringer](items []T, sep string) string {
519 var tmp []string
520 for _, i := range items {
521 tmp = append(tmp, i.String())
522 }
523 return strings.Join(tmp, ",")
524}
gio0eaf2712024-04-14 13:08:46 +0400525
526func networkMap(networks []Network) map[string]Network {
527 ret := make(map[string]Network)
528 for _, n := range networks {
529 ret[strings.ToLower(n.Name)] = n
530 }
531 return ret
532}