blob: 656b992209dfeece5816f9b7eb65f2c62e48ed4d [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
gio33222472025-08-04 12:02:34 +040043 EnvVar *AccessEnvVar
gio6ce44812025-05-17 07:31:54 +040044}
45
46func (a Access) MarshalJSON() ([]byte, error) {
47 var buf bytes.Buffer
48 switch a.Type {
49 case "https":
50 if err := json.NewEncoder(&buf).Encode(struct {
51 AccessHTTPS
52 Type string `json:"type"`
53 Name string `json:"name"`
54 }{*a.HTTPS, a.Type, a.Name}); err != nil {
55 return nil, err
56 }
57 case "ssh":
58 if err := json.NewEncoder(&buf).Encode(struct {
59 AccessSSH
60 Type string `json:"type"`
61 Name string `json:"name"`
62 }{*a.SSH, a.Type, a.Name}); err != nil {
63 return nil, err
64 }
65 case "tcp":
66 if err := json.NewEncoder(&buf).Encode(struct {
67 AccessTCP
68 Type string `json:"type"`
69 Name string `json:"name"`
70 }{*a.TCP, a.Type, a.Name}); err != nil {
71 return nil, err
72 }
73 case "udp":
74 if err := json.NewEncoder(&buf).Encode(struct {
75 AccessUDP
76 Type string `json:"type"`
77 Name string `json:"name"`
78 }{*a.UDP, a.Type, a.Name}); err != nil {
79 return nil, err
80 }
81 case "postgresql":
82 if err := json.NewEncoder(&buf).Encode(struct {
83 AccessPostgreSQL
84 Type string `json:"type"`
85 Name string `json:"name"`
86 }{*a.PostgreSQL, a.Type, a.Name}); err != nil {
87 return nil, err
88 }
89 case "mongodb":
90 if err := json.NewEncoder(&buf).Encode(struct {
91 AccessMongoDB
92 Type string `json:"type"`
93 Name string `json:"name"`
94 }{*a.MongoDB, a.Type, a.Name}); err != nil {
95 return nil, err
96 }
gio33222472025-08-04 12:02:34 +040097 case "env_var":
98 if err := json.NewEncoder(&buf).Encode(struct {
99 AccessEnvVar
100 Type string `json:"type"`
101 Name string `json:"name"`
102 }{*a.EnvVar, a.Type, a.Name}); err != nil {
103 return nil, err
104 }
gio6ce44812025-05-17 07:31:54 +0400105 default:
106 panic("MUST NOT REACH!")
107 }
108 return buf.Bytes(), nil
109}
110
111type AccessHTTPS struct {
gio85ddcdf2025-06-25 07:51:16 +0400112 Address string `json:"address"`
113 AgentName string `json:"agentName,omitempty"`
gio6ce44812025-05-17 07:31:54 +0400114}
115
116type AccessSSH struct {
117 Host string `json:"host"`
118 Port int `json:"port"`
119}
120
121type AccessTCP struct {
122 Host string `json:"host"`
123 Port int `json:"port"`
124}
125
126type AccessUDP struct {
127 Host string `json:"host"`
128 Port int `json:"port"`
129}
130
131type AccessPostgreSQL struct {
132 Host string `json:"host"`
133 Port int `json:"port"`
134 Database string `json:"database"`
135 Username string `json:"username"`
136 Password string `json:"password"`
137}
138
139type AccessMongoDB struct {
140 Host string `json:"host"`
141 Port int `json:"port"`
142 Database string `json:"database"`
143 Username string `json:"username"`
144 Password string `json:"password"`
145}
146
gio33222472025-08-04 12:02:34 +0400147type AccessEnvVar struct {
148 Var string `json:"var"`
149}
150
gio212f8002025-07-08 14:28:43 +0400151type EnvVar struct {
152 Name string `json:"name"`
153 Value string `json:"value"`
154}
155
gioe72b54f2024-04-22 10:44:41 +0400156type rendered struct {
giof8843412024-05-22 16:38:05 +0400157 Name string
158 Readme string
giof6ad2982024-08-23 17:42:49 +0400159 Cluster string
160 Namespaces []Namespace
giof8843412024-05-22 16:38:05 +0400161 Resources CueAppData
162 HelmCharts HelmCharts
163 ContainerImages map[string]ContainerImage
164 Ports []PortForward
giof6ad2982024-08-23 17:42:49 +0400165 ClusterProxies map[string]ClusterProxy
giof8843412024-05-22 16:38:05 +0400166 Data CueAppData
167 URL string
168 Help []HelpDocument
169 Icon string
gio6ce44812025-05-17 07:31:54 +0400170 Access []Access
gio212f8002025-07-08 14:28:43 +0400171 EnvVars []EnvVar
gio94904702024-07-26 16:58:34 +0400172 Raw []byte
Davit Tabidze56f86a42024-04-09 19:15:25 +0400173}
174
giof6ad2982024-08-23 17:42:49 +0400175type Namespace struct {
176 Name string `json:"name"`
177 Kubeconfig string `json:"kubeconfig,omitempty"`
178}
179
Davit Tabidze56f86a42024-04-09 19:15:25 +0400180type HelpDocument struct {
181 Title string
182 Contents string
183 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400184}
185
giof8843412024-05-22 16:38:05 +0400186type ContainerImage struct {
187 Registry string `json:"registry"`
188 Repository string `json:"repository"`
189 Name string `json:"name"`
190 Tag string `json:"tag"`
191}
192
193type helmChartRef struct {
194 Kind string `json:"kind"`
195}
196
197type HelmCharts struct {
198 Git map[string]HelmChartGitRepo
199}
200
201type HelmChartGitRepo struct {
202 Address string `json:"address"`
203 Branch string `json:"branch"`
204 Path string `json:"path"`
205}
206
gioe72b54f2024-04-22 10:44:41 +0400207type EnvAppRendered struct {
208 rendered
209 Config AppInstanceConfig
210}
211
212type InfraAppRendered struct {
213 rendered
214 Config InfraAppInstanceConfig
215}
216
giof6ad2982024-08-23 17:42:49 +0400217type ClusterProxy struct {
218 From string `json:"from"`
219 To string `json:"to"`
220}
221
gio3cdee592024-04-17 10:15:56 +0400222type PortForward struct {
giod78896a2025-04-10 07:42:13 +0400223 Cluster string `json:"clusterName,omitempty"`
224 Network Network `json:"network"`
225 Protocol string `json:"protocol"`
226 Port int `json:"port"`
227 Service struct {
giof4344632025-04-08 20:04:35 +0400228 Name string `json:"name"`
229 Namespace string `json:"namespace,omitempty"`
230 Port int `json:"port"`
gio802311e2024-11-04 08:37:34 +0400231 } `json:"service"`
gio3cdee592024-04-17 10:15:56 +0400232}
233
234type AppType int
235
236const (
237 AppTypeInfra AppType = iota
238 AppTypeEnv
239)
240
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400241type App interface {
242 Name() string
gio44f621b2024-04-29 09:44:38 +0400243 Type() AppType
244 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400245 Description() string
246 Icon() template.HTML
247 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400248 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400249}
250
251type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400252 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
253 PublicIP []net.IP `json:"publicIP,omitempty"`
254 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
255 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400256}
257
gio7841f4f2024-07-26 19:53:49 +0400258type InfraNetwork struct {
259 Name string `json:"name,omitempty"`
260 IngressClass string `json:"ingressClass,omitempty"`
261 CertificateIssuer string `json:"certificateIssuer,omitempty"`
262 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
263 ReservePortAddr string `json:"reservePortAddr,omitempty"`
264 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
265}
266
gio3cdee592024-04-17 10:15:56 +0400267type InfraApp interface {
268 App
gio7841f4f2024-07-26 19:53:49 +0400269 Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400270}
271
272type EnvNetwork struct {
273 DNS net.IP `json:"dns,omitempty"`
274 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
275 Ingress net.IP `json:"ingress,omitempty"`
276 Headscale net.IP `json:"headscale,omitempty"`
277 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
278 ServicesTo net.IP `json:"servicesTo,omitempty"`
279}
280
281func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
282 addr, err := netip.ParseAddr(subnet.String())
283 if err != nil {
284 return EnvNetwork{}, err
285 }
286 if !addr.Is4() {
287 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
288 }
289 dns := addr.Next()
290 ingress := dns.Next()
291 headscale := ingress.Next()
292 b := addr.AsSlice()
293 if b[3] != 0 {
294 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
295 }
296 b[3] = 10
297 servicesFrom, ok := netip.AddrFromSlice(b)
298 if !ok {
299 return EnvNetwork{}, fmt.Errorf("Must not reach")
300 }
301 b[3] = 254
302 servicesTo, ok := netip.AddrFromSlice(b)
303 if !ok {
304 return EnvNetwork{}, fmt.Errorf("Must not reach")
305 }
306 b[3] = b[2]
307 b[2] = b[1]
308 b[0] = 10
309 b[1] = 44
310 dnsInClusterIP, ok := netip.AddrFromSlice(b)
311 if !ok {
312 return EnvNetwork{}, fmt.Errorf("Must not reach")
313 }
314 return EnvNetwork{
315 DNS: net.ParseIP(dns.String()),
316 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
317 Ingress: net.ParseIP(ingress.String()),
318 Headscale: net.ParseIP(headscale.String()),
319 ServicesFrom: net.ParseIP(servicesFrom.String()),
320 ServicesTo: net.ParseIP(servicesTo.String()),
321 }, nil
gio3cdee592024-04-17 10:15:56 +0400322}
323
gioe72b54f2024-04-22 10:44:41 +0400324type EnvConfig struct {
325 Id string `json:"id,omitempty"`
326 InfraName string `json:"pcloudEnvName,omitempty"`
327 Domain string `json:"domain,omitempty"`
328 PrivateDomain string `json:"privateDomain,omitempty"`
329 ContactEmail string `json:"contactEmail,omitempty"`
330 AdminPublicKey string `json:"adminPublicKey,omitempty"`
331 PublicIP []net.IP `json:"publicIP,omitempty"`
332 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
333 NamespacePrefix string `json:"namespacePrefix,omitempty"`
334 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400335}
336
337type EnvApp interface {
338 App
gio36b23b32024-08-25 12:20:54 +0400339 Render(
340 release Release,
341 env EnvConfig,
342 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400343 clusters []Cluster,
gio36b23b32024-08-25 12:20:54 +0400344 values map[string]any,
345 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400346 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +0400347 ) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400348}
349
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400350type cueApp struct {
351 name string
352 description string
353 icon template.HTML
354 namespace string
355 schema Schema
gio308105e2024-04-19 13:12:13 +0400356 cfg cue.Value
357 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400358}
359
gio308105e2024-04-19 13:12:13 +0400360type CueAppData map[string][]byte
361
362func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
363 ctx := cuecontext.New()
364 buildCtx := build.NewContext()
365 cfg := &load.Config{
366 Context: buildCtx,
367 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400368 }
gio308105e2024-04-19 13:12:13 +0400369 names := make([]string, 0)
370 for n, b := range data {
371 a := fmt.Sprintf("/%s", n)
372 names = append(names, a)
373 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
374 }
375 instances := load.Instances(names, cfg)
376 for _, inst := range instances {
377 if inst.Err != nil {
378 return cue.Value{}, inst.Err
379 }
380 }
381 if len(instances) != 1 {
382 return cue.Value{}, fmt.Errorf("invalid")
383 }
384 ret := ctx.BuildInstance(instances[0])
gioc81a8472024-09-24 13:06:19 +0200385 if err := ret.Err(); err != nil {
386 return cue.Value{}, err
gio308105e2024-04-19 13:12:13 +0400387 }
388 if err := ret.Validate(); err != nil {
389 return cue.Value{}, err
390 }
391 return ret, nil
392}
393
394func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400395 cfg := struct {
396 Name string `json:"name"`
397 Namespace string `json:"namespace"`
398 Description string `json:"description"`
399 Icon string `json:"icon"`
400 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400401 if err := config.Decode(&cfg); err != nil {
402 return cueApp{}, err
403 }
gio44f621b2024-04-29 09:44:38 +0400404 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400405 if err != nil {
406 return cueApp{}, err
407 }
408 return cueApp{
409 name: cfg.Name,
410 description: cfg.Description,
411 icon: template.HTML(cfg.Icon),
412 namespace: cfg.Namespace,
413 schema: schema,
414 cfg: config,
gio308105e2024-04-19 13:12:13 +0400415 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400416 }, nil
417}
418
gio308105e2024-04-19 13:12:13 +0400419func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
420 config, err := ParseCueAppConfig(data)
421 if err != nil {
gio6481c902025-05-20 16:16:30 +0400422 return cueApp{}, fmt.Errorf(errors.Details(err, nil))
gio308105e2024-04-19 13:12:13 +0400423 }
gio33222472025-08-04 12:02:34 +0400424 if err := config.Err(); err != nil {
425 return cueApp{}, fmt.Errorf(errors.Details(err, nil))
426 }
gio308105e2024-04-19 13:12:13 +0400427 return newCueApp(config, data)
428}
429
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400430func (a cueApp) Name() string {
431 return a.name
432}
433
gio44f621b2024-04-29 09:44:38 +0400434func (a cueApp) Slug() string {
435 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
436}
437
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400438func (a cueApp) Description() string {
439 return a.description
440}
441
442func (a cueApp) Icon() template.HTML {
443 return a.icon
444}
445
446func (a cueApp) Schema() Schema {
447 return a.schema
448}
449
gioef01fbb2024-04-12 16:52:59 +0400450func (a cueApp) Namespace() string {
451 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400452}
453
gioe72b54f2024-04-22 10:44:41 +0400454func (a cueApp) render(values map[string]any) (rendered, error) {
455 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400456 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400457 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400458 HelmCharts: HelmCharts{
459 Git: make(map[string]HelmChartGitRepo),
460 },
461 ContainerImages: make(map[string]ContainerImage),
462 Ports: make([]PortForward, 0),
463 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400464 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400465 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400466 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400467 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400468 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400469 ctx := a.cfg.Context()
470 d := ctx.CompileBytes(buf.Bytes())
471 res := a.cfg.Unify(d).Eval()
472 if err := res.Err(); err != nil {
gioa1f29472025-05-14 13:05:05 +0400473 return rendered{}, fmt.Errorf(errors.Details(err, nil))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400474 }
475 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400476 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400477 }
gio308105e2024-04-19 13:12:13 +0400478 full, err := json.MarshalIndent(res, "", "\t")
479 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400480 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400481 }
gio94904702024-07-26 16:58:34 +0400482 ret.Raw = full
gio308105e2024-04-19 13:12:13 +0400483 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400484 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
485 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400486 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400487 }
488 ret.Readme = readme
giof6ad2982024-08-23 17:42:49 +0400489 res.LookupPath(cue.ParsePath("input.cluster.name")).Decode(&ret.Cluster)
490 if err := res.LookupPath(cue.ParsePath("output.clusterProxy")).Decode(&ret.ClusterProxies); err != nil {
491 return rendered{}, err
492 }
493 if err := res.LookupPath(cue.ParsePath("namespaces")).Decode(&ret.Namespaces); err != nil {
494 return rendered{}, err
495 }
gio802311e2024-11-04 08:37:34 +0400496 if err := res.LookupPath(cue.ParsePath("output.openPort")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400497 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400498 }
gio0eaf2712024-04-14 13:08:46 +0400499 {
gio212f8002025-07-08 14:28:43 +0400500 envVars := []string{}
501 if err := res.LookupPath(cue.ParsePath("envVars")).Decode(&envVars); err != nil {
502 return rendered{}, err
503 }
504 for _, ev := range envVars {
505 items := strings.SplitN(ev, "=", 2)
506 if len(items) != 2 {
507 panic(ev)
508 }
509 ret.EnvVars = append(ret.EnvVars, EnvVar{items[0], items[1]})
510 }
511 }
512 {
gio7fbd4ad2024-08-27 10:06:39 +0400513 charts := res.LookupPath(cue.ParsePath("output.charts"))
giof8843412024-05-22 16:38:05 +0400514 i, err := charts.Fields()
515 if err != nil {
516 return rendered{}, err
517 }
518 for i.Next() {
519 var chartRef helmChartRef
520 if err := i.Value().Decode(&chartRef); err != nil {
521 return rendered{}, err
522 }
523 if chartRef.Kind == "GitRepository" {
524 var chart HelmChartGitRepo
525 if err := i.Value().Decode(&chart); err != nil {
526 return rendered{}, err
527 }
528 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
529 }
530 }
531 }
532 {
gio7fbd4ad2024-08-27 10:06:39 +0400533 images := res.LookupPath(cue.ParsePath("output.images"))
giof8843412024-05-22 16:38:05 +0400534 i, err := images.Fields()
535 if err != nil {
536 return rendered{}, err
537 }
538 for i.Next() {
539 var img ContainerImage
540 if err := i.Value().Decode(&img); err != nil {
541 return rendered{}, err
542 }
543 ret.ContainerImages[cleanName(i.Selector().String())] = img
544 }
545 }
546 {
gio7fbd4ad2024-08-27 10:06:39 +0400547 helm := res.LookupPath(cue.ParsePath("output.helm"))
548 i, err := helm.Fields()
gio0eaf2712024-04-14 13:08:46 +0400549 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400550 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400551 }
552 for i.Next() {
553 if contents, err := cueyaml.Encode(i.Value()); err != nil {
554 return rendered{}, err
555 } else {
556 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
557 ret.Resources[name] = contents
558 }
559 }
560 }
561 {
562 resources := res.LookupPath(cue.ParsePath("resources"))
563 i, err := resources.Fields()
564 if err != nil {
565 return rendered{}, err
566 }
567 for i.Next() {
568 if contents, err := cueyaml.Encode(i.Value()); err != nil {
569 return rendered{}, err
570 } else {
571 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
572 ret.Resources[name] = contents
573 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400574 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400575 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400576 helpValue := res.LookupPath(cue.ParsePath("help"))
577 if helpValue.Exists() {
578 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400579 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400580 }
581 }
582 url, err := res.LookupPath(cue.ParsePath("url")).String()
583 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400584 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400585 }
gio09a3e5b2024-04-26 14:11:06 +0400586 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400587 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
588 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400589 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400590 }
591 ret.Icon = icon
gio6ce44812025-05-17 07:31:54 +0400592 access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
593 if err != nil {
594 return rendered{}, err
595 }
596 ret.Access = access
597 return ret, nil
598}
599
600func extractAccessInternal(v cue.Value) ([]Access, error) {
601 ret := []Access{}
602 a := v.LookupPath(cue.ParsePath("access"))
603 if err := a.Err(); err != nil {
604 return nil, err
605 }
606 i, err := a.List()
607 if err != nil {
608 return nil, err
609 }
610 for i.Next() {
611 n := i.Value().LookupPath(cue.ParsePath("name"))
612 if err := n.Err(); err != nil {
613 return nil, err
614 }
615 nn, err := n.String()
616 if err != nil {
617 return nil, err
618 }
619 t := i.Value().LookupPath(cue.ParsePath("type"))
620 if err := t.Err(); err != nil {
621 return nil, err
622 }
623 d, err := t.String()
624 if err != nil {
625 return nil, err
626 }
627 switch d {
628 case "https":
629 {
630 var q AccessHTTPS
631 if err := i.Value().Decode(&q); err != nil {
632 return nil, err
633 }
634 ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
635 }
636 case "ssh":
637 {
638 var q AccessSSH
639 if err := i.Value().Decode(&q); err != nil {
640 return nil, err
641 }
642 ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
643 }
644 case "tcp":
645 {
646 var q AccessTCP
647 if err := i.Value().Decode(&q); err != nil {
648 return nil, err
649 }
650 ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
651 }
652 case "udp":
653 {
654 var q AccessUDP
655 if err := i.Value().Decode(&q); err != nil {
656 return nil, err
657 }
658 ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
659 }
660 case "postgresql":
661 {
662 var q AccessPostgreSQL
663 if err := i.Value().Decode(&q); err != nil {
664 return nil, err
665 }
666 ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
667 }
668 case "mongodb":
669 {
670 var q AccessMongoDB
671 if err := i.Value().Decode(&q); err != nil {
672 return nil, err
673 }
674 ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
675 }
gio33222472025-08-04 12:02:34 +0400676 case "env_var":
677 {
678 var q AccessEnvVar
679 if err := i.Value().Decode(&q); err != nil {
680 return nil, err
681 }
682 ret = append(ret, Access{Type: "env_var", Name: nn, EnvVar: &q})
683 }
gio6ce44812025-05-17 07:31:54 +0400684 }
685 }
686 for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
687 subout := v.LookupPath(cue.ParsePath(sub))
688 if subout.Err() != nil {
689 continue
690 }
691 if a, err := extractAccess(subout); err != nil {
692 return nil, err
693 } else {
694 ret = append(ret, a...)
695 }
696 }
697 return ret, nil
698}
699
700func extractAccess(v cue.Value) ([]Access, error) {
701 if err := v.Err(); err != nil {
702 return nil, err
703 }
704 i, err := v.Fields()
705 if err != nil {
706 return nil, err
707 }
708 ret := []Access{}
709 for i.Next() {
710 if a, err := extractAccessInternal(i.Value()); err != nil {
711 return nil, fmt.Errorf(errors.Details(err, nil))
712 } else {
713 ret = append(ret, a...)
714 }
715 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400716 return ret, nil
717}
718
gio3cdee592024-04-17 10:15:56 +0400719type cueEnvApp struct {
720 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400721}
722
gio308105e2024-04-19 13:12:13 +0400723func NewCueEnvApp(data CueAppData) (EnvApp, error) {
724 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400725 if err != nil {
726 return nil, err
727 }
gio3cdee592024-04-17 10:15:56 +0400728 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400729}
730
gio0eaf2712024-04-14 13:08:46 +0400731func NewDodoApp(appCfg []byte) (EnvApp, error) {
732 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400733 "app.cue": appCfg,
gio33222472025-08-04 12:02:34 +0400734 "base.cue": cueBaseConfig,
giofc9c4ea2024-06-26 13:46:53 +0400735 "dodo.cue": dodoAppCue,
gio33222472025-08-04 12:02:34 +0400736 "env.cue": cueEnvAppGlobal,
gio0eaf2712024-04-14 13:08:46 +0400737 })
738}
739
gio3cdee592024-04-17 10:15:56 +0400740func (a cueEnvApp) Type() AppType {
741 return AppTypeEnv
742}
743
giofc441e32024-11-11 16:26:14 +0400744func merge(d map[string]any, v map[string]any) map[string]any {
745 ret := map[string]any{}
746 for k, val := range d {
747 if vv, ok := v[k]; ok && vv != nil {
748 if mv, ok := val.(map[string]any); ok {
749 // TODO(gio): check that it is actually map
gio28356152025-07-24 17:20:56 +0400750 mm, ok := vv.(map[string]any)
751 if !ok {
752 // TODO(gio): handle #Network and others
753 ret[k] = vv
754 } else {
755 ret[k] = merge(mv, mm)
756 }
giofc441e32024-11-11 16:26:14 +0400757 } else {
758 ret[k] = vv
759 }
760 } else {
761 ret[k] = val
762 }
763 }
764 for k, v := range v {
765 if v != nil {
766 if _, ok := d[k]; !ok {
gio838bcb82025-05-15 19:39:04 +0400767 ret[k] = v
giofc441e32024-11-11 16:26:14 +0400768 }
769 }
770 }
771 return ret
772}
773
giocb34ad22024-07-11 08:01:13 +0400774func (a cueEnvApp) Render(
775 release Release,
776 env EnvConfig,
777 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400778 clusters []Cluster,
giocb34ad22024-07-11 08:01:13 +0400779 values map[string]any,
780 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400781 vpnKeyGen VPNAPIClient,
giocb34ad22024-07-11 08:01:13 +0400782) (EnvAppRendered, error) {
giofc441e32024-11-11 16:26:14 +0400783 dv, err := ExtractDefaultValues(a.cueApp.cfg.LookupPath(cue.ParsePath("input")))
784 if err != nil {
785 return EnvAppRendered{}, err
786 }
gio842db3f2025-05-30 11:57:20 +0400787 if dv == nil {
788 dv = map[string]any{}
789 }
giofc441e32024-11-11 16:26:14 +0400790 mv := merge(dv.(map[string]any), values)
gio842db3f2025-05-30 11:57:20 +0400791 derived, err := deriveValues(mv, mv, a.Schema(), networks, clusters, vpnKeyGen)
gio3cdee592024-04-17 10:15:56 +0400792 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400793 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400794 }
giof8843412024-05-22 16:38:05 +0400795 if charts == nil {
796 charts = make(map[string]helmv2.HelmChartTemplateSpec)
797 }
giof15b9da2024-09-19 06:59:16 +0400798 if clusters == nil {
799 clusters = []Cluster{}
800 }
gio3cdee592024-04-17 10:15:56 +0400801 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400802 "global": env,
803 "release": release,
804 "input": derived,
805 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400806 "networks": NetworkMap(networks),
giof15b9da2024-09-19 06:59:16 +0400807 "clusters": clusters,
gio3cdee592024-04-17 10:15:56 +0400808 })
809 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400810 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400811 }
gioe72b54f2024-04-22 10:44:41 +0400812 return EnvAppRendered{
813 rendered: ret,
814 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400815 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400816 Env: env,
817 Release: release,
818 Values: values,
819 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400820 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400821 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400822 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400823 },
824 }, nil
gio3cdee592024-04-17 10:15:56 +0400825}
826
827type cueInfraApp struct {
828 cueApp
829}
830
gio308105e2024-04-19 13:12:13 +0400831func NewCueInfraApp(data CueAppData) (InfraApp, error) {
832 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400833 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400834 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400835 }
gio3cdee592024-04-17 10:15:56 +0400836 return cueInfraApp{app}, nil
837}
838
839func (a cueInfraApp) Type() AppType {
840 return AppTypeInfra
841}
842
gio7841f4f2024-07-26 19:53:49 +0400843func (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 +0400844 if charts == nil {
845 charts = make(map[string]helmv2.HelmChartTemplateSpec)
846 }
gioe72b54f2024-04-22 10:44:41 +0400847 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400848 "global": infra,
849 "release": release,
850 "input": values,
851 "localCharts": charts,
gio7841f4f2024-07-26 19:53:49 +0400852 "networks": InfraNetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400853 })
gioe72b54f2024-04-22 10:44:41 +0400854 if err != nil {
855 return InfraAppRendered{}, err
856 }
857 return InfraAppRendered{
858 rendered: ret,
859 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400860 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400861 Infra: infra,
862 Release: release,
863 Values: values,
864 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400865 URL: ret.URL,
866 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400867 },
868 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400869}
870
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400871func cleanName(s string) string {
872 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400873}
gioe72b54f2024-04-22 10:44:41 +0400874
875func join[T fmt.Stringer](items []T, sep string) string {
876 var tmp []string
877 for _, i := range items {
878 tmp = append(tmp, i.String())
879 }
880 return strings.Join(tmp, ",")
881}
gio0eaf2712024-04-14 13:08:46 +0400882
gio5e49bb62024-07-20 10:43:19 +0400883func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400884 ret := make(map[string]Network)
885 for _, n := range networks {
886 ret[strings.ToLower(n.Name)] = n
887 }
888 return ret
889}
gio7841f4f2024-07-26 19:53:49 +0400890
891func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
892 ret := make(map[string]InfraNetwork)
893 for _, n := range networks {
894 ret[strings.ToLower(n.Name)] = n
895 }
896 return ret
897}