blob: c9c7cedbb4d6509be05917a40ad3e27d58f00695 [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
gio212f8002025-07-08 14:28:43 +0400138type EnvVar struct {
139 Name string `json:"name"`
140 Value string `json:"value"`
141}
142
gioe72b54f2024-04-22 10:44:41 +0400143type rendered struct {
giof8843412024-05-22 16:38:05 +0400144 Name string
145 Readme string
giof6ad2982024-08-23 17:42:49 +0400146 Cluster string
147 Namespaces []Namespace
giof8843412024-05-22 16:38:05 +0400148 Resources CueAppData
149 HelmCharts HelmCharts
150 ContainerImages map[string]ContainerImage
151 Ports []PortForward
giof6ad2982024-08-23 17:42:49 +0400152 ClusterProxies map[string]ClusterProxy
giof8843412024-05-22 16:38:05 +0400153 Data CueAppData
154 URL string
155 Help []HelpDocument
156 Icon string
gio6ce44812025-05-17 07:31:54 +0400157 Access []Access
gio212f8002025-07-08 14:28:43 +0400158 EnvVars []EnvVar
gio94904702024-07-26 16:58:34 +0400159 Raw []byte
Davit Tabidze56f86a42024-04-09 19:15:25 +0400160}
161
giof6ad2982024-08-23 17:42:49 +0400162type Namespace struct {
163 Name string `json:"name"`
164 Kubeconfig string `json:"kubeconfig,omitempty"`
165}
166
Davit Tabidze56f86a42024-04-09 19:15:25 +0400167type HelpDocument struct {
168 Title string
169 Contents string
170 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400171}
172
giof8843412024-05-22 16:38:05 +0400173type ContainerImage struct {
174 Registry string `json:"registry"`
175 Repository string `json:"repository"`
176 Name string `json:"name"`
177 Tag string `json:"tag"`
178}
179
180type helmChartRef struct {
181 Kind string `json:"kind"`
182}
183
184type HelmCharts struct {
185 Git map[string]HelmChartGitRepo
186}
187
188type HelmChartGitRepo struct {
189 Address string `json:"address"`
190 Branch string `json:"branch"`
191 Path string `json:"path"`
192}
193
gioe72b54f2024-04-22 10:44:41 +0400194type EnvAppRendered struct {
195 rendered
196 Config AppInstanceConfig
197}
198
199type InfraAppRendered struct {
200 rendered
201 Config InfraAppInstanceConfig
202}
203
giof6ad2982024-08-23 17:42:49 +0400204type ClusterProxy struct {
205 From string `json:"from"`
206 To string `json:"to"`
207}
208
gio3cdee592024-04-17 10:15:56 +0400209type PortForward struct {
giod78896a2025-04-10 07:42:13 +0400210 Cluster string `json:"clusterName,omitempty"`
211 Network Network `json:"network"`
212 Protocol string `json:"protocol"`
213 Port int `json:"port"`
214 Service struct {
giof4344632025-04-08 20:04:35 +0400215 Name string `json:"name"`
216 Namespace string `json:"namespace,omitempty"`
217 Port int `json:"port"`
gio802311e2024-11-04 08:37:34 +0400218 } `json:"service"`
gio3cdee592024-04-17 10:15:56 +0400219}
220
221type AppType int
222
223const (
224 AppTypeInfra AppType = iota
225 AppTypeEnv
226)
227
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400228type App interface {
229 Name() string
gio44f621b2024-04-29 09:44:38 +0400230 Type() AppType
231 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400232 Description() string
233 Icon() template.HTML
234 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400235 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400236}
237
238type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400239 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
240 PublicIP []net.IP `json:"publicIP,omitempty"`
241 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
242 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400243}
244
gio7841f4f2024-07-26 19:53:49 +0400245type InfraNetwork struct {
246 Name string `json:"name,omitempty"`
247 IngressClass string `json:"ingressClass,omitempty"`
248 CertificateIssuer string `json:"certificateIssuer,omitempty"`
249 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
250 ReservePortAddr string `json:"reservePortAddr,omitempty"`
251 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
252}
253
gio3cdee592024-04-17 10:15:56 +0400254type InfraApp interface {
255 App
gio7841f4f2024-07-26 19:53:49 +0400256 Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400257}
258
259type EnvNetwork struct {
260 DNS net.IP `json:"dns,omitempty"`
261 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
262 Ingress net.IP `json:"ingress,omitempty"`
263 Headscale net.IP `json:"headscale,omitempty"`
264 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
265 ServicesTo net.IP `json:"servicesTo,omitempty"`
266}
267
268func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
269 addr, err := netip.ParseAddr(subnet.String())
270 if err != nil {
271 return EnvNetwork{}, err
272 }
273 if !addr.Is4() {
274 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
275 }
276 dns := addr.Next()
277 ingress := dns.Next()
278 headscale := ingress.Next()
279 b := addr.AsSlice()
280 if b[3] != 0 {
281 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
282 }
283 b[3] = 10
284 servicesFrom, ok := netip.AddrFromSlice(b)
285 if !ok {
286 return EnvNetwork{}, fmt.Errorf("Must not reach")
287 }
288 b[3] = 254
289 servicesTo, ok := netip.AddrFromSlice(b)
290 if !ok {
291 return EnvNetwork{}, fmt.Errorf("Must not reach")
292 }
293 b[3] = b[2]
294 b[2] = b[1]
295 b[0] = 10
296 b[1] = 44
297 dnsInClusterIP, ok := netip.AddrFromSlice(b)
298 if !ok {
299 return EnvNetwork{}, fmt.Errorf("Must not reach")
300 }
301 return EnvNetwork{
302 DNS: net.ParseIP(dns.String()),
303 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
304 Ingress: net.ParseIP(ingress.String()),
305 Headscale: net.ParseIP(headscale.String()),
306 ServicesFrom: net.ParseIP(servicesFrom.String()),
307 ServicesTo: net.ParseIP(servicesTo.String()),
308 }, nil
gio3cdee592024-04-17 10:15:56 +0400309}
310
gioe72b54f2024-04-22 10:44:41 +0400311type EnvConfig struct {
312 Id string `json:"id,omitempty"`
313 InfraName string `json:"pcloudEnvName,omitempty"`
314 Domain string `json:"domain,omitempty"`
315 PrivateDomain string `json:"privateDomain,omitempty"`
316 ContactEmail string `json:"contactEmail,omitempty"`
317 AdminPublicKey string `json:"adminPublicKey,omitempty"`
318 PublicIP []net.IP `json:"publicIP,omitempty"`
319 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
320 NamespacePrefix string `json:"namespacePrefix,omitempty"`
321 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400322}
323
324type EnvApp interface {
325 App
gio36b23b32024-08-25 12:20:54 +0400326 Render(
327 release Release,
328 env EnvConfig,
329 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400330 clusters []Cluster,
gio36b23b32024-08-25 12:20:54 +0400331 values map[string]any,
332 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400333 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +0400334 ) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400335}
336
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400337type cueApp struct {
338 name string
339 description string
340 icon template.HTML
341 namespace string
342 schema Schema
gio308105e2024-04-19 13:12:13 +0400343 cfg cue.Value
344 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400345}
346
gio308105e2024-04-19 13:12:13 +0400347type CueAppData map[string][]byte
348
349func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
350 ctx := cuecontext.New()
351 buildCtx := build.NewContext()
352 cfg := &load.Config{
353 Context: buildCtx,
354 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400355 }
gio308105e2024-04-19 13:12:13 +0400356 names := make([]string, 0)
357 for n, b := range data {
358 a := fmt.Sprintf("/%s", n)
359 names = append(names, a)
360 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
361 }
362 instances := load.Instances(names, cfg)
363 for _, inst := range instances {
364 if inst.Err != nil {
365 return cue.Value{}, inst.Err
366 }
367 }
368 if len(instances) != 1 {
369 return cue.Value{}, fmt.Errorf("invalid")
370 }
371 ret := ctx.BuildInstance(instances[0])
gioc81a8472024-09-24 13:06:19 +0200372 if err := ret.Err(); err != nil {
373 return cue.Value{}, err
gio308105e2024-04-19 13:12:13 +0400374 }
375 if err := ret.Validate(); err != nil {
376 return cue.Value{}, err
377 }
378 return ret, nil
379}
380
381func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400382 cfg := struct {
383 Name string `json:"name"`
384 Namespace string `json:"namespace"`
385 Description string `json:"description"`
386 Icon string `json:"icon"`
387 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400388 if err := config.Decode(&cfg); err != nil {
389 return cueApp{}, err
390 }
gio44f621b2024-04-29 09:44:38 +0400391 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400392 if err != nil {
393 return cueApp{}, err
394 }
395 return cueApp{
396 name: cfg.Name,
397 description: cfg.Description,
398 icon: template.HTML(cfg.Icon),
399 namespace: cfg.Namespace,
400 schema: schema,
401 cfg: config,
gio308105e2024-04-19 13:12:13 +0400402 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400403 }, nil
404}
405
gio308105e2024-04-19 13:12:13 +0400406func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
407 config, err := ParseCueAppConfig(data)
408 if err != nil {
gio6481c902025-05-20 16:16:30 +0400409 return cueApp{}, fmt.Errorf(errors.Details(err, nil))
gio308105e2024-04-19 13:12:13 +0400410 }
411 return newCueApp(config, data)
412}
413
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400414func (a cueApp) Name() string {
415 return a.name
416}
417
gio44f621b2024-04-29 09:44:38 +0400418func (a cueApp) Slug() string {
419 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
420}
421
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400422func (a cueApp) Description() string {
423 return a.description
424}
425
426func (a cueApp) Icon() template.HTML {
427 return a.icon
428}
429
430func (a cueApp) Schema() Schema {
431 return a.schema
432}
433
gioef01fbb2024-04-12 16:52:59 +0400434func (a cueApp) Namespace() string {
435 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400436}
437
gioe72b54f2024-04-22 10:44:41 +0400438func (a cueApp) render(values map[string]any) (rendered, error) {
439 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400440 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400441 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400442 HelmCharts: HelmCharts{
443 Git: make(map[string]HelmChartGitRepo),
444 },
445 ContainerImages: make(map[string]ContainerImage),
446 Ports: make([]PortForward, 0),
447 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400448 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400449 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400450 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400451 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400452 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400453 ctx := a.cfg.Context()
454 d := ctx.CompileBytes(buf.Bytes())
455 res := a.cfg.Unify(d).Eval()
456 if err := res.Err(); err != nil {
gioa1f29472025-05-14 13:05:05 +0400457 return rendered{}, fmt.Errorf(errors.Details(err, nil))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400458 }
459 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400460 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400461 }
gio308105e2024-04-19 13:12:13 +0400462 full, err := json.MarshalIndent(res, "", "\t")
463 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400464 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400465 }
gio94904702024-07-26 16:58:34 +0400466 ret.Raw = full
gio308105e2024-04-19 13:12:13 +0400467 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400468 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
469 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400470 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400471 }
472 ret.Readme = readme
giof6ad2982024-08-23 17:42:49 +0400473 res.LookupPath(cue.ParsePath("input.cluster.name")).Decode(&ret.Cluster)
474 if err := res.LookupPath(cue.ParsePath("output.clusterProxy")).Decode(&ret.ClusterProxies); err != nil {
475 return rendered{}, err
476 }
477 if err := res.LookupPath(cue.ParsePath("namespaces")).Decode(&ret.Namespaces); err != nil {
478 return rendered{}, err
479 }
gio802311e2024-11-04 08:37:34 +0400480 if err := res.LookupPath(cue.ParsePath("output.openPort")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400481 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400482 }
gio0eaf2712024-04-14 13:08:46 +0400483 {
gio212f8002025-07-08 14:28:43 +0400484 envVars := []string{}
485 if err := res.LookupPath(cue.ParsePath("envVars")).Decode(&envVars); err != nil {
486 return rendered{}, err
487 }
488 for _, ev := range envVars {
489 items := strings.SplitN(ev, "=", 2)
490 if len(items) != 2 {
491 panic(ev)
492 }
493 ret.EnvVars = append(ret.EnvVars, EnvVar{items[0], items[1]})
494 }
495 }
496 {
gio7fbd4ad2024-08-27 10:06:39 +0400497 charts := res.LookupPath(cue.ParsePath("output.charts"))
giof8843412024-05-22 16:38:05 +0400498 i, err := charts.Fields()
499 if err != nil {
500 return rendered{}, err
501 }
502 for i.Next() {
503 var chartRef helmChartRef
504 if err := i.Value().Decode(&chartRef); err != nil {
505 return rendered{}, err
506 }
507 if chartRef.Kind == "GitRepository" {
508 var chart HelmChartGitRepo
509 if err := i.Value().Decode(&chart); err != nil {
510 return rendered{}, err
511 }
512 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
513 }
514 }
515 }
516 {
gio7fbd4ad2024-08-27 10:06:39 +0400517 images := res.LookupPath(cue.ParsePath("output.images"))
giof8843412024-05-22 16:38:05 +0400518 i, err := images.Fields()
519 if err != nil {
520 return rendered{}, err
521 }
522 for i.Next() {
523 var img ContainerImage
524 if err := i.Value().Decode(&img); err != nil {
525 return rendered{}, err
526 }
527 ret.ContainerImages[cleanName(i.Selector().String())] = img
528 }
529 }
530 {
gio7fbd4ad2024-08-27 10:06:39 +0400531 helm := res.LookupPath(cue.ParsePath("output.helm"))
532 i, err := helm.Fields()
gio0eaf2712024-04-14 13:08:46 +0400533 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400534 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400535 }
536 for i.Next() {
537 if contents, err := cueyaml.Encode(i.Value()); err != nil {
538 return rendered{}, err
539 } else {
540 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
541 ret.Resources[name] = contents
542 }
543 }
544 }
545 {
546 resources := res.LookupPath(cue.ParsePath("resources"))
547 i, err := resources.Fields()
548 if err != nil {
549 return rendered{}, err
550 }
551 for i.Next() {
552 if contents, err := cueyaml.Encode(i.Value()); err != nil {
553 return rendered{}, err
554 } else {
555 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
556 ret.Resources[name] = contents
557 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400558 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400559 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400560 helpValue := res.LookupPath(cue.ParsePath("help"))
561 if helpValue.Exists() {
562 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400563 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400564 }
565 }
566 url, err := res.LookupPath(cue.ParsePath("url")).String()
567 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400568 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400569 }
gio09a3e5b2024-04-26 14:11:06 +0400570 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400571 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
572 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400573 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400574 }
575 ret.Icon = icon
gio6ce44812025-05-17 07:31:54 +0400576 access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
577 if err != nil {
578 return rendered{}, err
579 }
580 ret.Access = access
581 return ret, nil
582}
583
584func extractAccessInternal(v cue.Value) ([]Access, error) {
585 ret := []Access{}
586 a := v.LookupPath(cue.ParsePath("access"))
587 if err := a.Err(); err != nil {
588 return nil, err
589 }
590 i, err := a.List()
591 if err != nil {
592 return nil, err
593 }
594 for i.Next() {
595 n := i.Value().LookupPath(cue.ParsePath("name"))
596 if err := n.Err(); err != nil {
597 return nil, err
598 }
599 nn, err := n.String()
600 if err != nil {
601 return nil, err
602 }
603 t := i.Value().LookupPath(cue.ParsePath("type"))
604 if err := t.Err(); err != nil {
605 return nil, err
606 }
607 d, err := t.String()
608 if err != nil {
609 return nil, err
610 }
611 switch d {
612 case "https":
613 {
614 var q AccessHTTPS
615 if err := i.Value().Decode(&q); err != nil {
616 return nil, err
617 }
618 ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
619 }
620 case "ssh":
621 {
622 var q AccessSSH
623 if err := i.Value().Decode(&q); err != nil {
624 return nil, err
625 }
626 ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
627 }
628 case "tcp":
629 {
630 var q AccessTCP
631 if err := i.Value().Decode(&q); err != nil {
632 return nil, err
633 }
634 ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
635 }
636 case "udp":
637 {
638 var q AccessUDP
639 if err := i.Value().Decode(&q); err != nil {
640 return nil, err
641 }
642 ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
643 }
644 case "postgresql":
645 {
646 var q AccessPostgreSQL
647 if err := i.Value().Decode(&q); err != nil {
648 return nil, err
649 }
650 ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
651 }
652 case "mongodb":
653 {
654 var q AccessMongoDB
655 if err := i.Value().Decode(&q); err != nil {
656 return nil, err
657 }
658 ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
659 }
660 }
661 }
662 for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
663 subout := v.LookupPath(cue.ParsePath(sub))
664 if subout.Err() != nil {
665 continue
666 }
667 if a, err := extractAccess(subout); err != nil {
668 return nil, err
669 } else {
670 ret = append(ret, a...)
671 }
672 }
673 return ret, nil
674}
675
676func extractAccess(v cue.Value) ([]Access, error) {
677 if err := v.Err(); err != nil {
678 return nil, err
679 }
680 i, err := v.Fields()
681 if err != nil {
682 return nil, err
683 }
684 ret := []Access{}
685 for i.Next() {
686 if a, err := extractAccessInternal(i.Value()); err != nil {
687 return nil, fmt.Errorf(errors.Details(err, nil))
688 } else {
689 ret = append(ret, a...)
690 }
691 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400692 return ret, nil
693}
694
gio3cdee592024-04-17 10:15:56 +0400695type cueEnvApp struct {
696 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400697}
698
gio308105e2024-04-19 13:12:13 +0400699func NewCueEnvApp(data CueAppData) (EnvApp, error) {
700 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400701 if err != nil {
702 return nil, err
703 }
gio3cdee592024-04-17 10:15:56 +0400704 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400705}
706
gio0eaf2712024-04-14 13:08:46 +0400707func NewDodoApp(appCfg []byte) (EnvApp, error) {
708 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400709 "app.cue": appCfg,
710 "base.cue": []byte(cueBaseConfig),
711 "dodo.cue": dodoAppCue,
712 "env.cue": []byte(cueEnvAppGlobal),
gio0eaf2712024-04-14 13:08:46 +0400713 })
714}
715
gio3cdee592024-04-17 10:15:56 +0400716func (a cueEnvApp) Type() AppType {
717 return AppTypeEnv
718}
719
giofc441e32024-11-11 16:26:14 +0400720func merge(d map[string]any, v map[string]any) map[string]any {
721 ret := map[string]any{}
722 for k, val := range d {
723 if vv, ok := v[k]; ok && vv != nil {
724 if mv, ok := val.(map[string]any); ok {
725 // TODO(gio): check that it is actually map
gio28356152025-07-24 17:20:56 +0400726 mm, ok := vv.(map[string]any)
727 if !ok {
728 // TODO(gio): handle #Network and others
729 ret[k] = vv
730 } else {
731 ret[k] = merge(mv, mm)
732 }
giofc441e32024-11-11 16:26:14 +0400733 } else {
734 ret[k] = vv
735 }
736 } else {
737 ret[k] = val
738 }
739 }
740 for k, v := range v {
741 if v != nil {
742 if _, ok := d[k]; !ok {
gio838bcb82025-05-15 19:39:04 +0400743 ret[k] = v
giofc441e32024-11-11 16:26:14 +0400744 }
745 }
746 }
747 return ret
748}
749
giocb34ad22024-07-11 08:01:13 +0400750func (a cueEnvApp) Render(
751 release Release,
752 env EnvConfig,
753 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400754 clusters []Cluster,
giocb34ad22024-07-11 08:01:13 +0400755 values map[string]any,
756 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400757 vpnKeyGen VPNAPIClient,
giocb34ad22024-07-11 08:01:13 +0400758) (EnvAppRendered, error) {
giofc441e32024-11-11 16:26:14 +0400759 dv, err := ExtractDefaultValues(a.cueApp.cfg.LookupPath(cue.ParsePath("input")))
760 if err != nil {
761 return EnvAppRendered{}, err
762 }
gio842db3f2025-05-30 11:57:20 +0400763 if dv == nil {
764 dv = map[string]any{}
765 }
giofc441e32024-11-11 16:26:14 +0400766 mv := merge(dv.(map[string]any), values)
gio842db3f2025-05-30 11:57:20 +0400767 derived, err := deriveValues(mv, mv, a.Schema(), networks, clusters, vpnKeyGen)
gio3cdee592024-04-17 10:15:56 +0400768 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400769 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400770 }
giof8843412024-05-22 16:38:05 +0400771 if charts == nil {
772 charts = make(map[string]helmv2.HelmChartTemplateSpec)
773 }
giof15b9da2024-09-19 06:59:16 +0400774 if clusters == nil {
775 clusters = []Cluster{}
776 }
gio3cdee592024-04-17 10:15:56 +0400777 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400778 "global": env,
779 "release": release,
780 "input": derived,
781 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400782 "networks": NetworkMap(networks),
giof15b9da2024-09-19 06:59:16 +0400783 "clusters": clusters,
gio3cdee592024-04-17 10:15:56 +0400784 })
785 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400786 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400787 }
gioe72b54f2024-04-22 10:44:41 +0400788 return EnvAppRendered{
789 rendered: ret,
790 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400791 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400792 Env: env,
793 Release: release,
794 Values: values,
795 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400796 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400797 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400798 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400799 },
800 }, nil
gio3cdee592024-04-17 10:15:56 +0400801}
802
803type cueInfraApp struct {
804 cueApp
805}
806
gio308105e2024-04-19 13:12:13 +0400807func NewCueInfraApp(data CueAppData) (InfraApp, error) {
808 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400809 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400810 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400811 }
gio3cdee592024-04-17 10:15:56 +0400812 return cueInfraApp{app}, nil
813}
814
815func (a cueInfraApp) Type() AppType {
816 return AppTypeInfra
817}
818
gio7841f4f2024-07-26 19:53:49 +0400819func (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 +0400820 if charts == nil {
821 charts = make(map[string]helmv2.HelmChartTemplateSpec)
822 }
gioe72b54f2024-04-22 10:44:41 +0400823 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400824 "global": infra,
825 "release": release,
826 "input": values,
827 "localCharts": charts,
gio7841f4f2024-07-26 19:53:49 +0400828 "networks": InfraNetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400829 })
gioe72b54f2024-04-22 10:44:41 +0400830 if err != nil {
831 return InfraAppRendered{}, err
832 }
833 return InfraAppRendered{
834 rendered: ret,
835 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400836 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400837 Infra: infra,
838 Release: release,
839 Values: values,
840 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400841 URL: ret.URL,
842 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400843 },
844 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400845}
846
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400847func cleanName(s string) string {
848 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400849}
gioe72b54f2024-04-22 10:44:41 +0400850
851func join[T fmt.Stringer](items []T, sep string) string {
852 var tmp []string
853 for _, i := range items {
854 tmp = append(tmp, i.String())
855 }
856 return strings.Join(tmp, ",")
857}
gio0eaf2712024-04-14 13:08:46 +0400858
gio5e49bb62024-07-20 10:43:19 +0400859func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400860 ret := make(map[string]Network)
861 for _, n := range networks {
862 ret[strings.ToLower(n.Name)] = n
863 }
864 return ret
865}
gio7841f4f2024-07-26 19:53:49 +0400866
867func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
868 ret := make(map[string]InfraNetwork)
869 for _, n := range networks {
870 ret[strings.ToLower(n.Name)] = n
871 }
872 return ret
873}