blob: e0549647ada78489d56a65c722667fff36511a26 [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 {
103 Address string `json:"address"`
104}
105
106type AccessSSH struct {
107 Host string `json:"host"`
108 Port int `json:"port"`
109}
110
111type AccessTCP struct {
112 Host string `json:"host"`
113 Port int `json:"port"`
114}
115
116type AccessUDP struct {
117 Host string `json:"host"`
118 Port int `json:"port"`
119}
120
121type AccessPostgreSQL struct {
122 Host string `json:"host"`
123 Port int `json:"port"`
124 Database string `json:"database"`
125 Username string `json:"username"`
126 Password string `json:"password"`
127}
128
129type AccessMongoDB struct {
130 Host string `json:"host"`
131 Port int `json:"port"`
132 Database string `json:"database"`
133 Username string `json:"username"`
134 Password string `json:"password"`
135}
136
gioe72b54f2024-04-22 10:44:41 +0400137type rendered struct {
giof8843412024-05-22 16:38:05 +0400138 Name string
139 Readme string
giof6ad2982024-08-23 17:42:49 +0400140 Cluster string
141 Namespaces []Namespace
giof8843412024-05-22 16:38:05 +0400142 Resources CueAppData
143 HelmCharts HelmCharts
144 ContainerImages map[string]ContainerImage
145 Ports []PortForward
giof6ad2982024-08-23 17:42:49 +0400146 ClusterProxies map[string]ClusterProxy
giof8843412024-05-22 16:38:05 +0400147 Data CueAppData
148 URL string
149 Help []HelpDocument
150 Icon string
gio6ce44812025-05-17 07:31:54 +0400151 Access []Access
gio94904702024-07-26 16:58:34 +0400152 Raw []byte
Davit Tabidze56f86a42024-04-09 19:15:25 +0400153}
154
giof6ad2982024-08-23 17:42:49 +0400155type Namespace struct {
156 Name string `json:"name"`
157 Kubeconfig string `json:"kubeconfig,omitempty"`
158}
159
Davit Tabidze56f86a42024-04-09 19:15:25 +0400160type HelpDocument struct {
161 Title string
162 Contents string
163 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400164}
165
giof8843412024-05-22 16:38:05 +0400166type ContainerImage struct {
167 Registry string `json:"registry"`
168 Repository string `json:"repository"`
169 Name string `json:"name"`
170 Tag string `json:"tag"`
171}
172
173type helmChartRef struct {
174 Kind string `json:"kind"`
175}
176
177type HelmCharts struct {
178 Git map[string]HelmChartGitRepo
179}
180
181type HelmChartGitRepo struct {
182 Address string `json:"address"`
183 Branch string `json:"branch"`
184 Path string `json:"path"`
185}
186
gioe72b54f2024-04-22 10:44:41 +0400187type EnvAppRendered struct {
188 rendered
189 Config AppInstanceConfig
190}
191
192type InfraAppRendered struct {
193 rendered
194 Config InfraAppInstanceConfig
195}
196
giof6ad2982024-08-23 17:42:49 +0400197type ClusterProxy struct {
198 From string `json:"from"`
199 To string `json:"to"`
200}
201
gio3cdee592024-04-17 10:15:56 +0400202type PortForward struct {
giod78896a2025-04-10 07:42:13 +0400203 Cluster string `json:"clusterName,omitempty"`
204 Network Network `json:"network"`
205 Protocol string `json:"protocol"`
206 Port int `json:"port"`
207 Service struct {
giof4344632025-04-08 20:04:35 +0400208 Name string `json:"name"`
209 Namespace string `json:"namespace,omitempty"`
210 Port int `json:"port"`
gio802311e2024-11-04 08:37:34 +0400211 } `json:"service"`
gio3cdee592024-04-17 10:15:56 +0400212}
213
214type AppType int
215
216const (
217 AppTypeInfra AppType = iota
218 AppTypeEnv
219)
220
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400221type App interface {
222 Name() string
gio44f621b2024-04-29 09:44:38 +0400223 Type() AppType
224 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400225 Description() string
226 Icon() template.HTML
227 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400228 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400229}
230
231type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400232 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
233 PublicIP []net.IP `json:"publicIP,omitempty"`
234 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
235 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400236}
237
gio7841f4f2024-07-26 19:53:49 +0400238type InfraNetwork struct {
239 Name string `json:"name,omitempty"`
240 IngressClass string `json:"ingressClass,omitempty"`
241 CertificateIssuer string `json:"certificateIssuer,omitempty"`
242 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
243 ReservePortAddr string `json:"reservePortAddr,omitempty"`
244 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
245}
246
gio3cdee592024-04-17 10:15:56 +0400247type InfraApp interface {
248 App
gio7841f4f2024-07-26 19:53:49 +0400249 Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400250}
251
252type EnvNetwork struct {
253 DNS net.IP `json:"dns,omitempty"`
254 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
255 Ingress net.IP `json:"ingress,omitempty"`
256 Headscale net.IP `json:"headscale,omitempty"`
257 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
258 ServicesTo net.IP `json:"servicesTo,omitempty"`
259}
260
261func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
262 addr, err := netip.ParseAddr(subnet.String())
263 if err != nil {
264 return EnvNetwork{}, err
265 }
266 if !addr.Is4() {
267 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
268 }
269 dns := addr.Next()
270 ingress := dns.Next()
271 headscale := ingress.Next()
272 b := addr.AsSlice()
273 if b[3] != 0 {
274 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
275 }
276 b[3] = 10
277 servicesFrom, ok := netip.AddrFromSlice(b)
278 if !ok {
279 return EnvNetwork{}, fmt.Errorf("Must not reach")
280 }
281 b[3] = 254
282 servicesTo, ok := netip.AddrFromSlice(b)
283 if !ok {
284 return EnvNetwork{}, fmt.Errorf("Must not reach")
285 }
286 b[3] = b[2]
287 b[2] = b[1]
288 b[0] = 10
289 b[1] = 44
290 dnsInClusterIP, ok := netip.AddrFromSlice(b)
291 if !ok {
292 return EnvNetwork{}, fmt.Errorf("Must not reach")
293 }
294 return EnvNetwork{
295 DNS: net.ParseIP(dns.String()),
296 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
297 Ingress: net.ParseIP(ingress.String()),
298 Headscale: net.ParseIP(headscale.String()),
299 ServicesFrom: net.ParseIP(servicesFrom.String()),
300 ServicesTo: net.ParseIP(servicesTo.String()),
301 }, nil
gio3cdee592024-04-17 10:15:56 +0400302}
303
gioe72b54f2024-04-22 10:44:41 +0400304type EnvConfig struct {
305 Id string `json:"id,omitempty"`
306 InfraName string `json:"pcloudEnvName,omitempty"`
307 Domain string `json:"domain,omitempty"`
308 PrivateDomain string `json:"privateDomain,omitempty"`
309 ContactEmail string `json:"contactEmail,omitempty"`
310 AdminPublicKey string `json:"adminPublicKey,omitempty"`
311 PublicIP []net.IP `json:"publicIP,omitempty"`
312 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
313 NamespacePrefix string `json:"namespacePrefix,omitempty"`
314 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400315}
316
317type EnvApp interface {
318 App
gio36b23b32024-08-25 12:20:54 +0400319 Render(
320 release Release,
321 env EnvConfig,
322 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400323 clusters []Cluster,
gio36b23b32024-08-25 12:20:54 +0400324 values map[string]any,
325 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400326 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +0400327 ) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400328}
329
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400330type cueApp struct {
331 name string
332 description string
333 icon template.HTML
334 namespace string
335 schema Schema
gio308105e2024-04-19 13:12:13 +0400336 cfg cue.Value
337 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400338}
339
gio308105e2024-04-19 13:12:13 +0400340type CueAppData map[string][]byte
341
342func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
343 ctx := cuecontext.New()
344 buildCtx := build.NewContext()
345 cfg := &load.Config{
346 Context: buildCtx,
347 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400348 }
gio308105e2024-04-19 13:12:13 +0400349 names := make([]string, 0)
350 for n, b := range data {
351 a := fmt.Sprintf("/%s", n)
352 names = append(names, a)
353 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
354 }
355 instances := load.Instances(names, cfg)
356 for _, inst := range instances {
357 if inst.Err != nil {
358 return cue.Value{}, inst.Err
359 }
360 }
361 if len(instances) != 1 {
362 return cue.Value{}, fmt.Errorf("invalid")
363 }
364 ret := ctx.BuildInstance(instances[0])
gioc81a8472024-09-24 13:06:19 +0200365 if err := ret.Err(); err != nil {
366 return cue.Value{}, err
gio308105e2024-04-19 13:12:13 +0400367 }
368 if err := ret.Validate(); err != nil {
369 return cue.Value{}, err
370 }
371 return ret, nil
372}
373
374func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400375 cfg := struct {
376 Name string `json:"name"`
377 Namespace string `json:"namespace"`
378 Description string `json:"description"`
379 Icon string `json:"icon"`
380 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400381 if err := config.Decode(&cfg); err != nil {
382 return cueApp{}, err
383 }
gio44f621b2024-04-29 09:44:38 +0400384 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400385 if err != nil {
386 return cueApp{}, err
387 }
388 return cueApp{
389 name: cfg.Name,
390 description: cfg.Description,
391 icon: template.HTML(cfg.Icon),
392 namespace: cfg.Namespace,
393 schema: schema,
394 cfg: config,
gio308105e2024-04-19 13:12:13 +0400395 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400396 }, nil
397}
398
gio308105e2024-04-19 13:12:13 +0400399func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
400 config, err := ParseCueAppConfig(data)
401 if err != nil {
gio6481c902025-05-20 16:16:30 +0400402 return cueApp{}, fmt.Errorf(errors.Details(err, nil))
gio308105e2024-04-19 13:12:13 +0400403 }
404 return newCueApp(config, data)
405}
406
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400407func (a cueApp) Name() string {
408 return a.name
409}
410
gio44f621b2024-04-29 09:44:38 +0400411func (a cueApp) Slug() string {
412 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
413}
414
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400415func (a cueApp) Description() string {
416 return a.description
417}
418
419func (a cueApp) Icon() template.HTML {
420 return a.icon
421}
422
423func (a cueApp) Schema() Schema {
424 return a.schema
425}
426
gioef01fbb2024-04-12 16:52:59 +0400427func (a cueApp) Namespace() string {
428 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400429}
430
gioe72b54f2024-04-22 10:44:41 +0400431func (a cueApp) render(values map[string]any) (rendered, error) {
432 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400433 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400434 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400435 HelmCharts: HelmCharts{
436 Git: make(map[string]HelmChartGitRepo),
437 },
438 ContainerImages: make(map[string]ContainerImage),
439 Ports: make([]PortForward, 0),
440 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400441 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400442 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400443 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400444 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400445 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400446 ctx := a.cfg.Context()
447 d := ctx.CompileBytes(buf.Bytes())
448 res := a.cfg.Unify(d).Eval()
449 if err := res.Err(); err != nil {
gioa1f29472025-05-14 13:05:05 +0400450 return rendered{}, fmt.Errorf(errors.Details(err, nil))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400451 }
452 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400453 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400454 }
gio308105e2024-04-19 13:12:13 +0400455 full, err := json.MarshalIndent(res, "", "\t")
456 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400457 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400458 }
gio94904702024-07-26 16:58:34 +0400459 ret.Raw = full
gio308105e2024-04-19 13:12:13 +0400460 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400461 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
462 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400463 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400464 }
465 ret.Readme = readme
giof6ad2982024-08-23 17:42:49 +0400466 res.LookupPath(cue.ParsePath("input.cluster.name")).Decode(&ret.Cluster)
467 if err := res.LookupPath(cue.ParsePath("output.clusterProxy")).Decode(&ret.ClusterProxies); err != nil {
468 return rendered{}, err
469 }
470 if err := res.LookupPath(cue.ParsePath("namespaces")).Decode(&ret.Namespaces); err != nil {
471 return rendered{}, err
472 }
gio802311e2024-11-04 08:37:34 +0400473 if err := res.LookupPath(cue.ParsePath("output.openPort")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400474 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400475 }
gio0eaf2712024-04-14 13:08:46 +0400476 {
gio7fbd4ad2024-08-27 10:06:39 +0400477 charts := res.LookupPath(cue.ParsePath("output.charts"))
giof8843412024-05-22 16:38:05 +0400478 i, err := charts.Fields()
479 if err != nil {
480 return rendered{}, err
481 }
482 for i.Next() {
483 var chartRef helmChartRef
484 if err := i.Value().Decode(&chartRef); err != nil {
485 return rendered{}, err
486 }
487 if chartRef.Kind == "GitRepository" {
488 var chart HelmChartGitRepo
489 if err := i.Value().Decode(&chart); err != nil {
490 return rendered{}, err
491 }
492 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
493 }
494 }
495 }
496 {
gio7fbd4ad2024-08-27 10:06:39 +0400497 images := res.LookupPath(cue.ParsePath("output.images"))
giof8843412024-05-22 16:38:05 +0400498 i, err := images.Fields()
499 if err != nil {
500 return rendered{}, err
501 }
502 for i.Next() {
503 var img ContainerImage
504 if err := i.Value().Decode(&img); err != nil {
505 return rendered{}, err
506 }
507 ret.ContainerImages[cleanName(i.Selector().String())] = img
508 }
509 }
510 {
gio7fbd4ad2024-08-27 10:06:39 +0400511 helm := res.LookupPath(cue.ParsePath("output.helm"))
512 i, err := helm.Fields()
gio0eaf2712024-04-14 13:08:46 +0400513 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400514 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400515 }
516 for i.Next() {
517 if contents, err := cueyaml.Encode(i.Value()); err != nil {
518 return rendered{}, err
519 } else {
520 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
521 ret.Resources[name] = contents
522 }
523 }
524 }
525 {
526 resources := res.LookupPath(cue.ParsePath("resources"))
527 i, err := resources.Fields()
528 if err != nil {
529 return rendered{}, err
530 }
531 for i.Next() {
532 if contents, err := cueyaml.Encode(i.Value()); err != nil {
533 return rendered{}, err
534 } else {
535 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
536 ret.Resources[name] = contents
537 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400538 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400539 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400540 helpValue := res.LookupPath(cue.ParsePath("help"))
541 if helpValue.Exists() {
542 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400543 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400544 }
545 }
546 url, err := res.LookupPath(cue.ParsePath("url")).String()
547 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400548 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400549 }
gio09a3e5b2024-04-26 14:11:06 +0400550 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400551 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
552 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400553 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400554 }
555 ret.Icon = icon
gio6ce44812025-05-17 07:31:54 +0400556 access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
557 if err != nil {
558 return rendered{}, err
559 }
560 ret.Access = access
561 return ret, nil
562}
563
564func extractAccessInternal(v cue.Value) ([]Access, error) {
565 ret := []Access{}
566 a := v.LookupPath(cue.ParsePath("access"))
567 if err := a.Err(); err != nil {
568 return nil, err
569 }
570 i, err := a.List()
571 if err != nil {
572 return nil, err
573 }
574 for i.Next() {
575 n := i.Value().LookupPath(cue.ParsePath("name"))
576 if err := n.Err(); err != nil {
577 return nil, err
578 }
579 nn, err := n.String()
580 if err != nil {
581 return nil, err
582 }
583 t := i.Value().LookupPath(cue.ParsePath("type"))
584 if err := t.Err(); err != nil {
585 return nil, err
586 }
587 d, err := t.String()
588 if err != nil {
589 return nil, err
590 }
591 switch d {
592 case "https":
593 {
594 var q AccessHTTPS
595 if err := i.Value().Decode(&q); err != nil {
596 return nil, err
597 }
598 ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
599 }
600 case "ssh":
601 {
602 var q AccessSSH
603 if err := i.Value().Decode(&q); err != nil {
604 return nil, err
605 }
606 ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
607 }
608 case "tcp":
609 {
610 var q AccessTCP
611 if err := i.Value().Decode(&q); err != nil {
612 return nil, err
613 }
614 ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
615 }
616 case "udp":
617 {
618 var q AccessUDP
619 if err := i.Value().Decode(&q); err != nil {
620 return nil, err
621 }
622 ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
623 }
624 case "postgresql":
625 {
626 var q AccessPostgreSQL
627 if err := i.Value().Decode(&q); err != nil {
628 return nil, err
629 }
630 ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
631 }
632 case "mongodb":
633 {
634 var q AccessMongoDB
635 if err := i.Value().Decode(&q); err != nil {
636 return nil, err
637 }
638 ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
639 }
640 }
641 }
642 for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
643 subout := v.LookupPath(cue.ParsePath(sub))
644 if subout.Err() != nil {
645 continue
646 }
647 if a, err := extractAccess(subout); err != nil {
648 return nil, err
649 } else {
650 ret = append(ret, a...)
651 }
652 }
653 return ret, nil
654}
655
656func extractAccess(v cue.Value) ([]Access, error) {
657 if err := v.Err(); err != nil {
658 return nil, err
659 }
660 i, err := v.Fields()
661 if err != nil {
662 return nil, err
663 }
664 ret := []Access{}
665 for i.Next() {
666 if a, err := extractAccessInternal(i.Value()); err != nil {
667 return nil, fmt.Errorf(errors.Details(err, nil))
668 } else {
669 ret = append(ret, a...)
670 }
671 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400672 return ret, nil
673}
674
gio3cdee592024-04-17 10:15:56 +0400675type cueEnvApp struct {
676 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400677}
678
gio308105e2024-04-19 13:12:13 +0400679func NewCueEnvApp(data CueAppData) (EnvApp, error) {
680 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400681 if err != nil {
682 return nil, err
683 }
gio3cdee592024-04-17 10:15:56 +0400684 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400685}
686
gio0eaf2712024-04-14 13:08:46 +0400687func NewDodoApp(appCfg []byte) (EnvApp, error) {
688 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400689 "app.cue": appCfg,
690 "base.cue": []byte(cueBaseConfig),
691 "dodo.cue": dodoAppCue,
692 "env.cue": []byte(cueEnvAppGlobal),
gio0eaf2712024-04-14 13:08:46 +0400693 })
694}
695
gio3cdee592024-04-17 10:15:56 +0400696func (a cueEnvApp) Type() AppType {
697 return AppTypeEnv
698}
699
giofc441e32024-11-11 16:26:14 +0400700func merge(d map[string]any, v map[string]any) map[string]any {
701 ret := map[string]any{}
702 for k, val := range d {
703 if vv, ok := v[k]; ok && vv != nil {
704 if mv, ok := val.(map[string]any); ok {
705 // TODO(gio): check that it is actually map
giofc441e32024-11-11 16:26:14 +0400706 ret[k] = merge(mv, vv.(map[string]any))
707 } else {
708 ret[k] = vv
709 }
710 } else {
711 ret[k] = val
712 }
713 }
714 for k, v := range v {
715 if v != nil {
716 if _, ok := d[k]; !ok {
gio838bcb82025-05-15 19:39:04 +0400717 ret[k] = v
giofc441e32024-11-11 16:26:14 +0400718 }
719 }
720 }
721 return ret
722}
723
giocb34ad22024-07-11 08:01:13 +0400724func (a cueEnvApp) Render(
725 release Release,
726 env EnvConfig,
727 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400728 clusters []Cluster,
giocb34ad22024-07-11 08:01:13 +0400729 values map[string]any,
730 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400731 vpnKeyGen VPNAPIClient,
giocb34ad22024-07-11 08:01:13 +0400732) (EnvAppRendered, error) {
giofc441e32024-11-11 16:26:14 +0400733 dv, err := ExtractDefaultValues(a.cueApp.cfg.LookupPath(cue.ParsePath("input")))
734 if err != nil {
735 return EnvAppRendered{}, err
736 }
gio842db3f2025-05-30 11:57:20 +0400737 if dv == nil {
738 dv = map[string]any{}
739 }
giofc441e32024-11-11 16:26:14 +0400740 mv := merge(dv.(map[string]any), values)
gio842db3f2025-05-30 11:57:20 +0400741 derived, err := deriveValues(mv, mv, a.Schema(), networks, clusters, vpnKeyGen)
gio3cdee592024-04-17 10:15:56 +0400742 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400743 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400744 }
giof8843412024-05-22 16:38:05 +0400745 if charts == nil {
746 charts = make(map[string]helmv2.HelmChartTemplateSpec)
747 }
giof15b9da2024-09-19 06:59:16 +0400748 if clusters == nil {
749 clusters = []Cluster{}
750 }
gio3cdee592024-04-17 10:15:56 +0400751 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400752 "global": env,
753 "release": release,
754 "input": derived,
755 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400756 "networks": NetworkMap(networks),
giof15b9da2024-09-19 06:59:16 +0400757 "clusters": clusters,
gio3cdee592024-04-17 10:15:56 +0400758 })
759 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400760 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400761 }
gioe72b54f2024-04-22 10:44:41 +0400762 return EnvAppRendered{
763 rendered: ret,
764 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400765 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400766 Env: env,
767 Release: release,
768 Values: values,
769 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400770 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400771 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400772 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400773 },
774 }, nil
gio3cdee592024-04-17 10:15:56 +0400775}
776
777type cueInfraApp struct {
778 cueApp
779}
780
gio308105e2024-04-19 13:12:13 +0400781func NewCueInfraApp(data CueAppData) (InfraApp, error) {
782 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400783 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400784 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400785 }
gio3cdee592024-04-17 10:15:56 +0400786 return cueInfraApp{app}, nil
787}
788
789func (a cueInfraApp) Type() AppType {
790 return AppTypeInfra
791}
792
gio7841f4f2024-07-26 19:53:49 +0400793func (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 +0400794 if charts == nil {
795 charts = make(map[string]helmv2.HelmChartTemplateSpec)
796 }
gioe72b54f2024-04-22 10:44:41 +0400797 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400798 "global": infra,
799 "release": release,
800 "input": values,
801 "localCharts": charts,
gio7841f4f2024-07-26 19:53:49 +0400802 "networks": InfraNetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400803 })
gioe72b54f2024-04-22 10:44:41 +0400804 if err != nil {
805 return InfraAppRendered{}, err
806 }
807 return InfraAppRendered{
808 rendered: ret,
809 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400810 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400811 Infra: infra,
812 Release: release,
813 Values: values,
814 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400815 URL: ret.URL,
816 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400817 },
818 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400819}
820
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400821func cleanName(s string) string {
822 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400823}
gioe72b54f2024-04-22 10:44:41 +0400824
825func join[T fmt.Stringer](items []T, sep string) string {
826 var tmp []string
827 for _, i := range items {
828 tmp = append(tmp, i.String())
829 }
830 return strings.Join(tmp, ",")
831}
gio0eaf2712024-04-14 13:08:46 +0400832
gio5e49bb62024-07-20 10:43:19 +0400833func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400834 ret := make(map[string]Network)
835 for _, n := range networks {
836 ret[strings.ToLower(n.Name)] = n
837 }
838 return ret
839}
gio7841f4f2024-07-26 19:53:49 +0400840
841func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
842 ret := make(map[string]InfraNetwork)
843 for _, n := range networks {
844 ret[strings.ToLower(n.Name)] = n
845 }
846 return ret
847}