blob: f9d3bc0833135500954793dd80bf1784ddd369a9 [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"
gioa1f29472025-05-14 13:05:05 +040016 "cuelang.org/go/cue/errors"
gio308105e2024-04-19 13:12:13 +040017 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040018 cueyaml "cuelang.org/go/encoding/yaml"
giof8843412024-05-22 16:38:05 +040019 helmv2 "github.com/fluxcd/helm-controller/api/v2"
giolekva8aa73e82022-07-09 11:34:39 +040020)
giolekva050609f2021-12-29 15:51:40 +040021
giof8843412024-05-22 16:38:05 +040022//go:embed app_configs/dodo_app.cue
23var dodoAppCue []byte
gio0eaf2712024-04-14 13:08:46 +040024
giof8843412024-05-22 16:38:05 +040025//go:embed app_configs/app_base.cue
26var cueBaseConfig []byte
gio0eaf2712024-04-14 13:08:46 +040027
giof8843412024-05-22 16:38:05 +040028//go:embed app_configs/app_global_env.cue
29var cueEnvAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040030
giof8843412024-05-22 16:38:05 +040031//go:embed app_configs/app_global_infra.cue
32var cueInfraAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040033
gio6ce44812025-05-17 07:31:54 +040034type Access struct {
35 Type string `json:"type"`
36 Name string `json:"name"`
37 HTTPS *AccessHTTPS
38 SSH *AccessSSH
39 TCP *AccessTCP
40 UDP *AccessUDP
41 PostgreSQL *AccessPostgreSQL
42 MongoDB *AccessMongoDB
43}
44
45func (a Access) MarshalJSON() ([]byte, error) {
46 var buf bytes.Buffer
47 switch a.Type {
48 case "https":
49 if err := json.NewEncoder(&buf).Encode(struct {
50 AccessHTTPS
51 Type string `json:"type"`
52 Name string `json:"name"`
53 }{*a.HTTPS, a.Type, a.Name}); err != nil {
54 return nil, err
55 }
56 case "ssh":
57 if err := json.NewEncoder(&buf).Encode(struct {
58 AccessSSH
59 Type string `json:"type"`
60 Name string `json:"name"`
61 }{*a.SSH, a.Type, a.Name}); err != nil {
62 return nil, err
63 }
64 case "tcp":
65 if err := json.NewEncoder(&buf).Encode(struct {
66 AccessTCP
67 Type string `json:"type"`
68 Name string `json:"name"`
69 }{*a.TCP, a.Type, a.Name}); err != nil {
70 return nil, err
71 }
72 case "udp":
73 if err := json.NewEncoder(&buf).Encode(struct {
74 AccessUDP
75 Type string `json:"type"`
76 Name string `json:"name"`
77 }{*a.UDP, a.Type, a.Name}); err != nil {
78 return nil, err
79 }
80 case "postgresql":
81 if err := json.NewEncoder(&buf).Encode(struct {
82 AccessPostgreSQL
83 Type string `json:"type"`
84 Name string `json:"name"`
85 }{*a.PostgreSQL, a.Type, a.Name}); err != nil {
86 return nil, err
87 }
88 case "mongodb":
89 if err := json.NewEncoder(&buf).Encode(struct {
90 AccessMongoDB
91 Type string `json:"type"`
92 Name string `json:"name"`
93 }{*a.MongoDB, a.Type, a.Name}); err != nil {
94 return nil, err
95 }
96 default:
97 panic("MUST NOT REACH!")
98 }
99 return buf.Bytes(), nil
100}
101
102type AccessHTTPS struct {
gio85ddcdf2025-06-25 07:51:16 +0400103 Address string `json:"address"`
104 AgentName string `json:"agentName,omitempty"`
gio6ce44812025-05-17 07:31:54 +0400105}
106
107type AccessSSH struct {
108 Host string `json:"host"`
109 Port int `json:"port"`
110}
111
112type AccessTCP struct {
113 Host string `json:"host"`
114 Port int `json:"port"`
115}
116
117type AccessUDP struct {
118 Host string `json:"host"`
119 Port int `json:"port"`
120}
121
122type AccessPostgreSQL struct {
123 Host string `json:"host"`
124 Port int `json:"port"`
125 Database string `json:"database"`
126 Username string `json:"username"`
127 Password string `json:"password"`
128}
129
130type AccessMongoDB struct {
131 Host string `json:"host"`
132 Port int `json:"port"`
133 Database string `json:"database"`
134 Username string `json:"username"`
135 Password string `json:"password"`
136}
137
gioe72b54f2024-04-22 10:44:41 +0400138type rendered struct {
giof8843412024-05-22 16:38:05 +0400139 Name string
140 Readme string
giof6ad2982024-08-23 17:42:49 +0400141 Cluster string
142 Namespaces []Namespace
giof8843412024-05-22 16:38:05 +0400143 Resources CueAppData
144 HelmCharts HelmCharts
145 ContainerImages map[string]ContainerImage
146 Ports []PortForward
giof6ad2982024-08-23 17:42:49 +0400147 ClusterProxies map[string]ClusterProxy
giof8843412024-05-22 16:38:05 +0400148 Data CueAppData
149 URL string
150 Help []HelpDocument
151 Icon string
gio6ce44812025-05-17 07:31:54 +0400152 Access []Access
gio94904702024-07-26 16:58:34 +0400153 Raw []byte
Davit Tabidze56f86a42024-04-09 19:15:25 +0400154}
155
giof6ad2982024-08-23 17:42:49 +0400156type Namespace struct {
157 Name string `json:"name"`
158 Kubeconfig string `json:"kubeconfig,omitempty"`
159}
160
Davit Tabidze56f86a42024-04-09 19:15:25 +0400161type HelpDocument struct {
162 Title string
163 Contents string
164 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400165}
166
giof8843412024-05-22 16:38:05 +0400167type ContainerImage struct {
168 Registry string `json:"registry"`
169 Repository string `json:"repository"`
170 Name string `json:"name"`
171 Tag string `json:"tag"`
172}
173
174type helmChartRef struct {
175 Kind string `json:"kind"`
176}
177
178type HelmCharts struct {
179 Git map[string]HelmChartGitRepo
180}
181
182type HelmChartGitRepo struct {
183 Address string `json:"address"`
184 Branch string `json:"branch"`
185 Path string `json:"path"`
186}
187
gioe72b54f2024-04-22 10:44:41 +0400188type EnvAppRendered struct {
189 rendered
190 Config AppInstanceConfig
191}
192
193type InfraAppRendered struct {
194 rendered
195 Config InfraAppInstanceConfig
196}
197
giof6ad2982024-08-23 17:42:49 +0400198type ClusterProxy struct {
199 From string `json:"from"`
200 To string `json:"to"`
201}
202
gio3cdee592024-04-17 10:15:56 +0400203type PortForward struct {
giod78896a2025-04-10 07:42:13 +0400204 Cluster string `json:"clusterName,omitempty"`
205 Network Network `json:"network"`
206 Protocol string `json:"protocol"`
207 Port int `json:"port"`
208 Service struct {
giof4344632025-04-08 20:04:35 +0400209 Name string `json:"name"`
210 Namespace string `json:"namespace,omitempty"`
211 Port int `json:"port"`
gio802311e2024-11-04 08:37:34 +0400212 } `json:"service"`
gio3cdee592024-04-17 10:15:56 +0400213}
214
215type AppType int
216
217const (
218 AppTypeInfra AppType = iota
219 AppTypeEnv
220)
221
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400222type App interface {
223 Name() string
gio44f621b2024-04-29 09:44:38 +0400224 Type() AppType
225 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400226 Description() string
227 Icon() template.HTML
228 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400229 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400230}
231
232type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400233 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
234 PublicIP []net.IP `json:"publicIP,omitempty"`
235 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
236 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400237}
238
gio7841f4f2024-07-26 19:53:49 +0400239type InfraNetwork struct {
240 Name string `json:"name,omitempty"`
241 IngressClass string `json:"ingressClass,omitempty"`
242 CertificateIssuer string `json:"certificateIssuer,omitempty"`
243 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
244 ReservePortAddr string `json:"reservePortAddr,omitempty"`
245 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
246}
247
gio3cdee592024-04-17 10:15:56 +0400248type InfraApp interface {
249 App
gio7841f4f2024-07-26 19:53:49 +0400250 Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400251}
252
253type EnvNetwork struct {
254 DNS net.IP `json:"dns,omitempty"`
255 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
256 Ingress net.IP `json:"ingress,omitempty"`
257 Headscale net.IP `json:"headscale,omitempty"`
258 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
259 ServicesTo net.IP `json:"servicesTo,omitempty"`
260}
261
262func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
263 addr, err := netip.ParseAddr(subnet.String())
264 if err != nil {
265 return EnvNetwork{}, err
266 }
267 if !addr.Is4() {
268 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
269 }
270 dns := addr.Next()
271 ingress := dns.Next()
272 headscale := ingress.Next()
273 b := addr.AsSlice()
274 if b[3] != 0 {
275 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
276 }
277 b[3] = 10
278 servicesFrom, ok := netip.AddrFromSlice(b)
279 if !ok {
280 return EnvNetwork{}, fmt.Errorf("Must not reach")
281 }
282 b[3] = 254
283 servicesTo, ok := netip.AddrFromSlice(b)
284 if !ok {
285 return EnvNetwork{}, fmt.Errorf("Must not reach")
286 }
287 b[3] = b[2]
288 b[2] = b[1]
289 b[0] = 10
290 b[1] = 44
291 dnsInClusterIP, ok := netip.AddrFromSlice(b)
292 if !ok {
293 return EnvNetwork{}, fmt.Errorf("Must not reach")
294 }
295 return EnvNetwork{
296 DNS: net.ParseIP(dns.String()),
297 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
298 Ingress: net.ParseIP(ingress.String()),
299 Headscale: net.ParseIP(headscale.String()),
300 ServicesFrom: net.ParseIP(servicesFrom.String()),
301 ServicesTo: net.ParseIP(servicesTo.String()),
302 }, nil
gio3cdee592024-04-17 10:15:56 +0400303}
304
gioe72b54f2024-04-22 10:44:41 +0400305type EnvConfig struct {
306 Id string `json:"id,omitempty"`
307 InfraName string `json:"pcloudEnvName,omitempty"`
308 Domain string `json:"domain,omitempty"`
309 PrivateDomain string `json:"privateDomain,omitempty"`
310 ContactEmail string `json:"contactEmail,omitempty"`
311 AdminPublicKey string `json:"adminPublicKey,omitempty"`
312 PublicIP []net.IP `json:"publicIP,omitempty"`
313 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
314 NamespacePrefix string `json:"namespacePrefix,omitempty"`
315 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400316}
317
318type EnvApp interface {
319 App
gio36b23b32024-08-25 12:20:54 +0400320 Render(
321 release Release,
322 env EnvConfig,
323 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400324 clusters []Cluster,
gio36b23b32024-08-25 12:20:54 +0400325 values map[string]any,
326 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400327 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +0400328 ) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400329}
330
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400331type cueApp struct {
332 name string
333 description string
334 icon template.HTML
335 namespace string
336 schema Schema
gio308105e2024-04-19 13:12:13 +0400337 cfg cue.Value
338 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400339}
340
gio308105e2024-04-19 13:12:13 +0400341type CueAppData map[string][]byte
342
343func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
344 ctx := cuecontext.New()
345 buildCtx := build.NewContext()
346 cfg := &load.Config{
347 Context: buildCtx,
348 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400349 }
gio308105e2024-04-19 13:12:13 +0400350 names := make([]string, 0)
351 for n, b := range data {
352 a := fmt.Sprintf("/%s", n)
353 names = append(names, a)
354 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
355 }
356 instances := load.Instances(names, cfg)
357 for _, inst := range instances {
358 if inst.Err != nil {
359 return cue.Value{}, inst.Err
360 }
361 }
362 if len(instances) != 1 {
363 return cue.Value{}, fmt.Errorf("invalid")
364 }
365 ret := ctx.BuildInstance(instances[0])
gioc81a8472024-09-24 13:06:19 +0200366 if err := ret.Err(); err != nil {
367 return cue.Value{}, err
gio308105e2024-04-19 13:12:13 +0400368 }
369 if err := ret.Validate(); err != nil {
370 return cue.Value{}, err
371 }
372 return ret, nil
373}
374
375func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400376 cfg := struct {
377 Name string `json:"name"`
378 Namespace string `json:"namespace"`
379 Description string `json:"description"`
380 Icon string `json:"icon"`
381 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400382 if err := config.Decode(&cfg); err != nil {
383 return cueApp{}, err
384 }
gio44f621b2024-04-29 09:44:38 +0400385 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400386 if err != nil {
387 return cueApp{}, err
388 }
389 return cueApp{
390 name: cfg.Name,
391 description: cfg.Description,
392 icon: template.HTML(cfg.Icon),
393 namespace: cfg.Namespace,
394 schema: schema,
395 cfg: config,
gio308105e2024-04-19 13:12:13 +0400396 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400397 }, nil
398}
399
gio308105e2024-04-19 13:12:13 +0400400func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
401 config, err := ParseCueAppConfig(data)
402 if err != nil {
gio6481c902025-05-20 16:16:30 +0400403 return cueApp{}, fmt.Errorf(errors.Details(err, nil))
gio308105e2024-04-19 13:12:13 +0400404 }
405 return newCueApp(config, data)
406}
407
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400408func (a cueApp) Name() string {
409 return a.name
410}
411
gio44f621b2024-04-29 09:44:38 +0400412func (a cueApp) Slug() string {
413 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
414}
415
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400416func (a cueApp) Description() string {
417 return a.description
418}
419
420func (a cueApp) Icon() template.HTML {
421 return a.icon
422}
423
424func (a cueApp) Schema() Schema {
425 return a.schema
426}
427
gioef01fbb2024-04-12 16:52:59 +0400428func (a cueApp) Namespace() string {
429 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400430}
431
gioe72b54f2024-04-22 10:44:41 +0400432func (a cueApp) render(values map[string]any) (rendered, error) {
433 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400434 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400435 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400436 HelmCharts: HelmCharts{
437 Git: make(map[string]HelmChartGitRepo),
438 },
439 ContainerImages: make(map[string]ContainerImage),
440 Ports: make([]PortForward, 0),
441 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400442 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400443 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400444 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400445 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400446 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400447 ctx := a.cfg.Context()
448 d := ctx.CompileBytes(buf.Bytes())
449 res := a.cfg.Unify(d).Eval()
450 if err := res.Err(); err != nil {
gioa1f29472025-05-14 13:05:05 +0400451 return rendered{}, fmt.Errorf(errors.Details(err, nil))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400452 }
453 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400454 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400455 }
gio308105e2024-04-19 13:12:13 +0400456 full, err := json.MarshalIndent(res, "", "\t")
457 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400458 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400459 }
gio94904702024-07-26 16:58:34 +0400460 ret.Raw = full
gio308105e2024-04-19 13:12:13 +0400461 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400462 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
463 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400464 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400465 }
466 ret.Readme = readme
giof6ad2982024-08-23 17:42:49 +0400467 res.LookupPath(cue.ParsePath("input.cluster.name")).Decode(&ret.Cluster)
468 if err := res.LookupPath(cue.ParsePath("output.clusterProxy")).Decode(&ret.ClusterProxies); err != nil {
469 return rendered{}, err
470 }
471 if err := res.LookupPath(cue.ParsePath("namespaces")).Decode(&ret.Namespaces); err != nil {
472 return rendered{}, err
473 }
gio802311e2024-11-04 08:37:34 +0400474 if err := res.LookupPath(cue.ParsePath("output.openPort")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400475 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400476 }
gio0eaf2712024-04-14 13:08:46 +0400477 {
gio7fbd4ad2024-08-27 10:06:39 +0400478 charts := res.LookupPath(cue.ParsePath("output.charts"))
giof8843412024-05-22 16:38:05 +0400479 i, err := charts.Fields()
480 if err != nil {
481 return rendered{}, err
482 }
483 for i.Next() {
484 var chartRef helmChartRef
485 if err := i.Value().Decode(&chartRef); err != nil {
486 return rendered{}, err
487 }
488 if chartRef.Kind == "GitRepository" {
489 var chart HelmChartGitRepo
490 if err := i.Value().Decode(&chart); err != nil {
491 return rendered{}, err
492 }
493 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
494 }
495 }
496 }
497 {
gio7fbd4ad2024-08-27 10:06:39 +0400498 images := res.LookupPath(cue.ParsePath("output.images"))
giof8843412024-05-22 16:38:05 +0400499 i, err := images.Fields()
500 if err != nil {
501 return rendered{}, err
502 }
503 for i.Next() {
504 var img ContainerImage
505 if err := i.Value().Decode(&img); err != nil {
506 return rendered{}, err
507 }
508 ret.ContainerImages[cleanName(i.Selector().String())] = img
509 }
510 }
511 {
gio7fbd4ad2024-08-27 10:06:39 +0400512 helm := res.LookupPath(cue.ParsePath("output.helm"))
513 i, err := helm.Fields()
gio0eaf2712024-04-14 13:08:46 +0400514 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400515 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400516 }
517 for i.Next() {
518 if contents, err := cueyaml.Encode(i.Value()); err != nil {
519 return rendered{}, err
520 } else {
521 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
522 ret.Resources[name] = contents
523 }
524 }
525 }
526 {
527 resources := res.LookupPath(cue.ParsePath("resources"))
528 i, err := resources.Fields()
529 if err != nil {
530 return rendered{}, err
531 }
532 for i.Next() {
533 if contents, err := cueyaml.Encode(i.Value()); err != nil {
534 return rendered{}, err
535 } else {
536 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
537 ret.Resources[name] = contents
538 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400539 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400540 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400541 helpValue := res.LookupPath(cue.ParsePath("help"))
542 if helpValue.Exists() {
543 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400544 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400545 }
546 }
547 url, err := res.LookupPath(cue.ParsePath("url")).String()
548 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400549 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400550 }
gio09a3e5b2024-04-26 14:11:06 +0400551 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400552 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
553 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400554 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400555 }
556 ret.Icon = icon
gio6ce44812025-05-17 07:31:54 +0400557 access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
558 if err != nil {
559 return rendered{}, err
560 }
561 ret.Access = access
562 return ret, nil
563}
564
565func extractAccessInternal(v cue.Value) ([]Access, error) {
566 ret := []Access{}
567 a := v.LookupPath(cue.ParsePath("access"))
568 if err := a.Err(); err != nil {
569 return nil, err
570 }
571 i, err := a.List()
572 if err != nil {
573 return nil, err
574 }
575 for i.Next() {
576 n := i.Value().LookupPath(cue.ParsePath("name"))
577 if err := n.Err(); err != nil {
578 return nil, err
579 }
580 nn, err := n.String()
581 if err != nil {
582 return nil, err
583 }
584 t := i.Value().LookupPath(cue.ParsePath("type"))
585 if err := t.Err(); err != nil {
586 return nil, err
587 }
588 d, err := t.String()
589 if err != nil {
590 return nil, err
591 }
592 switch d {
593 case "https":
594 {
595 var q AccessHTTPS
596 if err := i.Value().Decode(&q); err != nil {
597 return nil, err
598 }
599 ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
600 }
601 case "ssh":
602 {
603 var q AccessSSH
604 if err := i.Value().Decode(&q); err != nil {
605 return nil, err
606 }
607 ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
608 }
609 case "tcp":
610 {
611 var q AccessTCP
612 if err := i.Value().Decode(&q); err != nil {
613 return nil, err
614 }
615 ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
616 }
617 case "udp":
618 {
619 var q AccessUDP
620 if err := i.Value().Decode(&q); err != nil {
621 return nil, err
622 }
623 ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
624 }
625 case "postgresql":
626 {
627 var q AccessPostgreSQL
628 if err := i.Value().Decode(&q); err != nil {
629 return nil, err
630 }
631 ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
632 }
633 case "mongodb":
634 {
635 var q AccessMongoDB
636 if err := i.Value().Decode(&q); err != nil {
637 return nil, err
638 }
639 ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
640 }
641 }
642 }
643 for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
644 subout := v.LookupPath(cue.ParsePath(sub))
645 if subout.Err() != nil {
646 continue
647 }
648 if a, err := extractAccess(subout); err != nil {
649 return nil, err
650 } else {
651 ret = append(ret, a...)
652 }
653 }
654 return ret, nil
655}
656
657func extractAccess(v cue.Value) ([]Access, error) {
658 if err := v.Err(); err != nil {
659 return nil, err
660 }
661 i, err := v.Fields()
662 if err != nil {
663 return nil, err
664 }
665 ret := []Access{}
666 for i.Next() {
667 if a, err := extractAccessInternal(i.Value()); err != nil {
668 return nil, fmt.Errorf(errors.Details(err, nil))
669 } else {
670 ret = append(ret, a...)
671 }
672 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400673 return ret, nil
674}
675
gio3cdee592024-04-17 10:15:56 +0400676type cueEnvApp struct {
677 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400678}
679
gio308105e2024-04-19 13:12:13 +0400680func NewCueEnvApp(data CueAppData) (EnvApp, error) {
681 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400682 if err != nil {
683 return nil, err
684 }
gio3cdee592024-04-17 10:15:56 +0400685 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400686}
687
gio0eaf2712024-04-14 13:08:46 +0400688func NewDodoApp(appCfg []byte) (EnvApp, error) {
689 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400690 "app.cue": appCfg,
691 "base.cue": []byte(cueBaseConfig),
692 "dodo.cue": dodoAppCue,
693 "env.cue": []byte(cueEnvAppGlobal),
gio0eaf2712024-04-14 13:08:46 +0400694 })
695}
696
gio3cdee592024-04-17 10:15:56 +0400697func (a cueEnvApp) Type() AppType {
698 return AppTypeEnv
699}
700
giofc441e32024-11-11 16:26:14 +0400701func merge(d map[string]any, v map[string]any) map[string]any {
702 ret := map[string]any{}
703 for k, val := range d {
704 if vv, ok := v[k]; ok && vv != nil {
705 if mv, ok := val.(map[string]any); ok {
706 // TODO(gio): check that it is actually map
giofc441e32024-11-11 16:26:14 +0400707 ret[k] = merge(mv, vv.(map[string]any))
708 } else {
709 ret[k] = vv
710 }
711 } else {
712 ret[k] = val
713 }
714 }
715 for k, v := range v {
716 if v != nil {
717 if _, ok := d[k]; !ok {
gio838bcb82025-05-15 19:39:04 +0400718 ret[k] = v
giofc441e32024-11-11 16:26:14 +0400719 }
720 }
721 }
722 return ret
723}
724
giocb34ad22024-07-11 08:01:13 +0400725func (a cueEnvApp) Render(
726 release Release,
727 env EnvConfig,
728 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400729 clusters []Cluster,
giocb34ad22024-07-11 08:01:13 +0400730 values map[string]any,
731 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400732 vpnKeyGen VPNAPIClient,
giocb34ad22024-07-11 08:01:13 +0400733) (EnvAppRendered, error) {
giofc441e32024-11-11 16:26:14 +0400734 dv, err := ExtractDefaultValues(a.cueApp.cfg.LookupPath(cue.ParsePath("input")))
735 if err != nil {
736 return EnvAppRendered{}, err
737 }
gio842db3f2025-05-30 11:57:20 +0400738 if dv == nil {
739 dv = map[string]any{}
740 }
giofc441e32024-11-11 16:26:14 +0400741 mv := merge(dv.(map[string]any), values)
gio842db3f2025-05-30 11:57:20 +0400742 derived, err := deriveValues(mv, mv, a.Schema(), networks, clusters, vpnKeyGen)
gio3cdee592024-04-17 10:15:56 +0400743 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400744 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400745 }
giof8843412024-05-22 16:38:05 +0400746 if charts == nil {
747 charts = make(map[string]helmv2.HelmChartTemplateSpec)
748 }
giof15b9da2024-09-19 06:59:16 +0400749 if clusters == nil {
750 clusters = []Cluster{}
751 }
gio3cdee592024-04-17 10:15:56 +0400752 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400753 "global": env,
754 "release": release,
755 "input": derived,
756 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400757 "networks": NetworkMap(networks),
giof15b9da2024-09-19 06:59:16 +0400758 "clusters": clusters,
gio3cdee592024-04-17 10:15:56 +0400759 })
760 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400761 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400762 }
gioe72b54f2024-04-22 10:44:41 +0400763 return EnvAppRendered{
764 rendered: ret,
765 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400766 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400767 Env: env,
768 Release: release,
769 Values: values,
770 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400771 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400772 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400773 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400774 },
775 }, nil
gio3cdee592024-04-17 10:15:56 +0400776}
777
778type cueInfraApp struct {
779 cueApp
780}
781
gio308105e2024-04-19 13:12:13 +0400782func NewCueInfraApp(data CueAppData) (InfraApp, error) {
783 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400784 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400785 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400786 }
gio3cdee592024-04-17 10:15:56 +0400787 return cueInfraApp{app}, nil
788}
789
790func (a cueInfraApp) Type() AppType {
791 return AppTypeInfra
792}
793
gio7841f4f2024-07-26 19:53:49 +0400794func (a cueInfraApp) Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
giof8843412024-05-22 16:38:05 +0400795 if charts == nil {
796 charts = make(map[string]helmv2.HelmChartTemplateSpec)
797 }
gioe72b54f2024-04-22 10:44:41 +0400798 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400799 "global": infra,
800 "release": release,
801 "input": values,
802 "localCharts": charts,
gio7841f4f2024-07-26 19:53:49 +0400803 "networks": InfraNetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400804 })
gioe72b54f2024-04-22 10:44:41 +0400805 if err != nil {
806 return InfraAppRendered{}, err
807 }
808 return InfraAppRendered{
809 rendered: ret,
810 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400811 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400812 Infra: infra,
813 Release: release,
814 Values: values,
815 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400816 URL: ret.URL,
817 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400818 },
819 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400820}
821
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400822func cleanName(s string) string {
823 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400824}
gioe72b54f2024-04-22 10:44:41 +0400825
826func join[T fmt.Stringer](items []T, sep string) string {
827 var tmp []string
828 for _, i := range items {
829 tmp = append(tmp, i.String())
830 }
831 return strings.Join(tmp, ",")
832}
gio0eaf2712024-04-14 13:08:46 +0400833
gio5e49bb62024-07-20 10:43:19 +0400834func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400835 ret := make(map[string]Network)
836 for _, n := range networks {
837 ret[strings.ToLower(n.Name)] = n
838 }
839 return ret
840}
gio7841f4f2024-07-26 19:53:49 +0400841
842func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
843 ret := make(map[string]InfraNetwork)
844 for _, n := range networks {
845 ret[strings.ToLower(n.Name)] = n
846 }
847 return ret
848}