blob: e42a9df9ed18eb8df6ef1b4c102a912e6936526f [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"
giob7df27f2026-07-28 10:36:17 +04007 "errors"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +04008 "fmt"
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +04009 template "html/template"
gio3cdee592024-04-17 10:15:56 +040010 "net"
gioe72b54f2024-04-22 10:44:41 +040011 "net/netip"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040012 "strings"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040013
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040014 "cuelang.org/go/cue"
gio308105e2024-04-19 13:12:13 +040015 "cuelang.org/go/cue/build"
16 "cuelang.org/go/cue/cuecontext"
giob7df27f2026-07-28 10:36:17 +040017 cueerrors "cuelang.org/go/cue/errors"
gio308105e2024-04-19 13:12:13 +040018 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040019 cueyaml "cuelang.org/go/encoding/yaml"
giof8843412024-05-22 16:38:05 +040020 helmv2 "github.com/fluxcd/helm-controller/api/v2"
giolekva8aa73e82022-07-09 11:34:39 +040021)
giolekva050609f2021-12-29 15:51:40 +040022
giof8843412024-05-22 16:38:05 +040023//go:embed app_configs/dodo_app.cue
24var dodoAppCue []byte
gio0eaf2712024-04-14 13:08:46 +040025
giof8843412024-05-22 16:38:05 +040026//go:embed app_configs/app_base.cue
27var cueBaseConfig []byte
gio0eaf2712024-04-14 13:08:46 +040028
giof8843412024-05-22 16:38:05 +040029//go:embed app_configs/app_global_env.cue
30var cueEnvAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040031
giof8843412024-05-22 16:38:05 +040032//go:embed app_configs/app_global_infra.cue
33var cueInfraAppGlobal []byte
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040034
gio6ce44812025-05-17 07:31:54 +040035type Access struct {
36 Type string `json:"type"`
37 Name string `json:"name"`
38 HTTPS *AccessHTTPS
39 SSH *AccessSSH
40 TCP *AccessTCP
41 UDP *AccessUDP
42 PostgreSQL *AccessPostgreSQL
43 MongoDB *AccessMongoDB
gio33222472025-08-04 12:02:34 +040044 EnvVar *AccessEnvVar
gio6ce44812025-05-17 07:31:54 +040045}
46
47func (a Access) MarshalJSON() ([]byte, error) {
48 var buf bytes.Buffer
49 switch a.Type {
50 case "https":
51 if err := json.NewEncoder(&buf).Encode(struct {
52 AccessHTTPS
53 Type string `json:"type"`
54 Name string `json:"name"`
55 }{*a.HTTPS, a.Type, a.Name}); err != nil {
56 return nil, err
57 }
58 case "ssh":
59 if err := json.NewEncoder(&buf).Encode(struct {
60 AccessSSH
61 Type string `json:"type"`
62 Name string `json:"name"`
63 }{*a.SSH, a.Type, a.Name}); err != nil {
64 return nil, err
65 }
66 case "tcp":
67 if err := json.NewEncoder(&buf).Encode(struct {
68 AccessTCP
69 Type string `json:"type"`
70 Name string `json:"name"`
71 }{*a.TCP, a.Type, a.Name}); err != nil {
72 return nil, err
73 }
74 case "udp":
75 if err := json.NewEncoder(&buf).Encode(struct {
76 AccessUDP
77 Type string `json:"type"`
78 Name string `json:"name"`
79 }{*a.UDP, a.Type, a.Name}); err != nil {
80 return nil, err
81 }
82 case "postgresql":
83 if err := json.NewEncoder(&buf).Encode(struct {
84 AccessPostgreSQL
85 Type string `json:"type"`
86 Name string `json:"name"`
87 }{*a.PostgreSQL, a.Type, a.Name}); err != nil {
88 return nil, err
89 }
90 case "mongodb":
91 if err := json.NewEncoder(&buf).Encode(struct {
92 AccessMongoDB
93 Type string `json:"type"`
94 Name string `json:"name"`
95 }{*a.MongoDB, a.Type, a.Name}); err != nil {
96 return nil, err
97 }
gio33222472025-08-04 12:02:34 +040098 case "env_var":
99 if err := json.NewEncoder(&buf).Encode(struct {
100 AccessEnvVar
101 Type string `json:"type"`
102 Name string `json:"name"`
103 }{*a.EnvVar, a.Type, a.Name}); err != nil {
104 return nil, err
105 }
gio6ce44812025-05-17 07:31:54 +0400106 default:
107 panic("MUST NOT REACH!")
108 }
109 return buf.Bytes(), nil
110}
111
112type AccessHTTPS struct {
gio85ddcdf2025-06-25 07:51:16 +0400113 Address string `json:"address"`
114 AgentName string `json:"agentName,omitempty"`
gio6ce44812025-05-17 07:31:54 +0400115}
116
117type AccessSSH struct {
118 Host string `json:"host"`
119 Port int `json:"port"`
120}
121
122type AccessTCP struct {
123 Host string `json:"host"`
124 Port int `json:"port"`
125}
126
127type AccessUDP struct {
128 Host string `json:"host"`
129 Port int `json:"port"`
130}
131
132type AccessPostgreSQL struct {
133 Host string `json:"host"`
134 Port int `json:"port"`
135 Database string `json:"database"`
136 Username string `json:"username"`
137 Password string `json:"password"`
138}
139
140type AccessMongoDB struct {
141 Host string `json:"host"`
142 Port int `json:"port"`
143 Database string `json:"database"`
144 Username string `json:"username"`
145 Password string `json:"password"`
146}
147
gio33222472025-08-04 12:02:34 +0400148type AccessEnvVar struct {
149 Var string `json:"var"`
150}
151
gio212f8002025-07-08 14:28:43 +0400152type EnvVar struct {
153 Name string `json:"name"`
154 Value string `json:"value"`
155}
156
gioe72b54f2024-04-22 10:44:41 +0400157type rendered struct {
giof8843412024-05-22 16:38:05 +0400158 Name string
159 Readme string
giof6ad2982024-08-23 17:42:49 +0400160 Cluster string
161 Namespaces []Namespace
giof8843412024-05-22 16:38:05 +0400162 Resources CueAppData
163 HelmCharts HelmCharts
164 ContainerImages map[string]ContainerImage
165 Ports []PortForward
giof6ad2982024-08-23 17:42:49 +0400166 ClusterProxies map[string]ClusterProxy
giof8843412024-05-22 16:38:05 +0400167 Data CueAppData
168 URL string
169 Help []HelpDocument
170 Icon string
gio6ce44812025-05-17 07:31:54 +0400171 Access []Access
gio212f8002025-07-08 14:28:43 +0400172 EnvVars []EnvVar
gio94904702024-07-26 16:58:34 +0400173 Raw []byte
Davit Tabidze56f86a42024-04-09 19:15:25 +0400174}
175
giof6ad2982024-08-23 17:42:49 +0400176type Namespace struct {
177 Name string `json:"name"`
178 Kubeconfig string `json:"kubeconfig,omitempty"`
179}
180
Davit Tabidze56f86a42024-04-09 19:15:25 +0400181type HelpDocument struct {
182 Title string
183 Contents string
184 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400185}
186
giof8843412024-05-22 16:38:05 +0400187type ContainerImage struct {
188 Registry string `json:"registry"`
189 Repository string `json:"repository"`
190 Name string `json:"name"`
191 Tag string `json:"tag"`
192}
193
194type helmChartRef struct {
195 Kind string `json:"kind"`
196}
197
198type HelmCharts struct {
199 Git map[string]HelmChartGitRepo
200}
201
202type HelmChartGitRepo struct {
203 Address string `json:"address"`
204 Branch string `json:"branch"`
205 Path string `json:"path"`
206}
207
gioe72b54f2024-04-22 10:44:41 +0400208type EnvAppRendered struct {
209 rendered
210 Config AppInstanceConfig
211}
212
213type InfraAppRendered struct {
214 rendered
215 Config InfraAppInstanceConfig
216}
217
giof6ad2982024-08-23 17:42:49 +0400218type ClusterProxy struct {
219 From string `json:"from"`
220 To string `json:"to"`
221}
222
gio3cdee592024-04-17 10:15:56 +0400223type PortForward struct {
giod78896a2025-04-10 07:42:13 +0400224 Cluster string `json:"clusterName,omitempty"`
225 Network Network `json:"network"`
226 Protocol string `json:"protocol"`
227 Port int `json:"port"`
228 Service struct {
giof4344632025-04-08 20:04:35 +0400229 Name string `json:"name"`
230 Namespace string `json:"namespace,omitempty"`
231 Port int `json:"port"`
gio802311e2024-11-04 08:37:34 +0400232 } `json:"service"`
gio3cdee592024-04-17 10:15:56 +0400233}
234
235type AppType int
236
237const (
238 AppTypeInfra AppType = iota
239 AppTypeEnv
240)
241
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400242type App interface {
243 Name() string
gio44f621b2024-04-29 09:44:38 +0400244 Type() AppType
245 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400246 Description() string
247 Icon() template.HTML
248 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400249 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400250}
251
252type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400253 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
254 PublicIP []net.IP `json:"publicIP,omitempty"`
255 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
256 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400257}
258
gio7841f4f2024-07-26 19:53:49 +0400259type InfraNetwork struct {
260 Name string `json:"name,omitempty"`
261 IngressClass string `json:"ingressClass,omitempty"`
262 CertificateIssuer string `json:"certificateIssuer,omitempty"`
263 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
264 ReservePortAddr string `json:"reservePortAddr,omitempty"`
265 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
266}
267
gio3cdee592024-04-17 10:15:56 +0400268type InfraApp interface {
269 App
gio7841f4f2024-07-26 19:53:49 +0400270 Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
gioe72b54f2024-04-22 10:44:41 +0400271}
272
273type EnvNetwork struct {
274 DNS net.IP `json:"dns,omitempty"`
275 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
276 Ingress net.IP `json:"ingress,omitempty"`
277 Headscale net.IP `json:"headscale,omitempty"`
278 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
279 ServicesTo net.IP `json:"servicesTo,omitempty"`
280}
281
282func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
283 addr, err := netip.ParseAddr(subnet.String())
284 if err != nil {
285 return EnvNetwork{}, err
286 }
287 if !addr.Is4() {
288 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
289 }
290 dns := addr.Next()
291 ingress := dns.Next()
292 headscale := ingress.Next()
293 b := addr.AsSlice()
294 if b[3] != 0 {
295 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
296 }
297 b[3] = 10
298 servicesFrom, ok := netip.AddrFromSlice(b)
299 if !ok {
300 return EnvNetwork{}, fmt.Errorf("Must not reach")
301 }
302 b[3] = 254
303 servicesTo, ok := netip.AddrFromSlice(b)
304 if !ok {
305 return EnvNetwork{}, fmt.Errorf("Must not reach")
306 }
307 b[3] = b[2]
308 b[2] = b[1]
309 b[0] = 10
310 b[1] = 44
311 dnsInClusterIP, ok := netip.AddrFromSlice(b)
312 if !ok {
313 return EnvNetwork{}, fmt.Errorf("Must not reach")
314 }
315 return EnvNetwork{
316 DNS: net.ParseIP(dns.String()),
317 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
318 Ingress: net.ParseIP(ingress.String()),
319 Headscale: net.ParseIP(headscale.String()),
320 ServicesFrom: net.ParseIP(servicesFrom.String()),
321 ServicesTo: net.ParseIP(servicesTo.String()),
322 }, nil
gio3cdee592024-04-17 10:15:56 +0400323}
324
gioe72b54f2024-04-22 10:44:41 +0400325type EnvConfig struct {
326 Id string `json:"id,omitempty"`
327 InfraName string `json:"pcloudEnvName,omitempty"`
328 Domain string `json:"domain,omitempty"`
329 PrivateDomain string `json:"privateDomain,omitempty"`
330 ContactEmail string `json:"contactEmail,omitempty"`
331 AdminPublicKey string `json:"adminPublicKey,omitempty"`
332 PublicIP []net.IP `json:"publicIP,omitempty"`
333 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
334 NamespacePrefix string `json:"namespacePrefix,omitempty"`
335 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400336}
337
338type EnvApp interface {
339 App
gio36b23b32024-08-25 12:20:54 +0400340 Render(
341 release Release,
342 env EnvConfig,
343 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400344 clusters []Cluster,
gio36b23b32024-08-25 12:20:54 +0400345 values map[string]any,
346 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400347 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +0400348 ) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400349}
350
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400351type cueApp struct {
352 name string
353 description string
354 icon template.HTML
355 namespace string
356 schema Schema
gio308105e2024-04-19 13:12:13 +0400357 cfg cue.Value
358 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400359}
360
gio308105e2024-04-19 13:12:13 +0400361type CueAppData map[string][]byte
362
363func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
364 ctx := cuecontext.New()
365 buildCtx := build.NewContext()
366 cfg := &load.Config{
367 Context: buildCtx,
368 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400369 }
gio308105e2024-04-19 13:12:13 +0400370 names := make([]string, 0)
371 for n, b := range data {
372 a := fmt.Sprintf("/%s", n)
373 names = append(names, a)
374 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
375 }
376 instances := load.Instances(names, cfg)
377 for _, inst := range instances {
378 if inst.Err != nil {
379 return cue.Value{}, inst.Err
380 }
381 }
382 if len(instances) != 1 {
383 return cue.Value{}, fmt.Errorf("invalid")
384 }
385 ret := ctx.BuildInstance(instances[0])
gioc81a8472024-09-24 13:06:19 +0200386 if err := ret.Err(); err != nil {
387 return cue.Value{}, err
gio308105e2024-04-19 13:12:13 +0400388 }
389 if err := ret.Validate(); err != nil {
390 return cue.Value{}, err
391 }
392 return ret, nil
393}
394
395func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400396 cfg := struct {
397 Name string `json:"name"`
398 Namespace string `json:"namespace"`
399 Description string `json:"description"`
400 Icon string `json:"icon"`
401 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400402 if err := config.Decode(&cfg); err != nil {
403 return cueApp{}, err
404 }
gio44f621b2024-04-29 09:44:38 +0400405 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400406 if err != nil {
407 return cueApp{}, err
408 }
409 return cueApp{
410 name: cfg.Name,
411 description: cfg.Description,
412 icon: template.HTML(cfg.Icon),
413 namespace: cfg.Namespace,
414 schema: schema,
415 cfg: config,
gio308105e2024-04-19 13:12:13 +0400416 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400417 }, nil
418}
419
gio308105e2024-04-19 13:12:13 +0400420func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
421 config, err := ParseCueAppConfig(data)
422 if err != nil {
giob7df27f2026-07-28 10:36:17 +0400423 return cueApp{}, errors.New(cueerrors.Details(err, nil))
gio308105e2024-04-19 13:12:13 +0400424 }
gio33222472025-08-04 12:02:34 +0400425 if err := config.Err(); err != nil {
giob7df27f2026-07-28 10:36:17 +0400426 return cueApp{}, errors.New(cueerrors.Details(err, nil))
gio33222472025-08-04 12:02:34 +0400427 }
gio308105e2024-04-19 13:12:13 +0400428 return newCueApp(config, data)
429}
430
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400431func (a cueApp) Name() string {
432 return a.name
433}
434
gio44f621b2024-04-29 09:44:38 +0400435func (a cueApp) Slug() string {
436 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
437}
438
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400439func (a cueApp) Description() string {
440 return a.description
441}
442
443func (a cueApp) Icon() template.HTML {
444 return a.icon
445}
446
447func (a cueApp) Schema() Schema {
448 return a.schema
449}
450
gioef01fbb2024-04-12 16:52:59 +0400451func (a cueApp) Namespace() string {
452 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400453}
454
gioe72b54f2024-04-22 10:44:41 +0400455func (a cueApp) render(values map[string]any) (rendered, error) {
456 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400457 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400458 Resources: make(CueAppData),
giof8843412024-05-22 16:38:05 +0400459 HelmCharts: HelmCharts{
460 Git: make(map[string]HelmChartGitRepo),
461 },
462 ContainerImages: make(map[string]ContainerImage),
463 Ports: make([]PortForward, 0),
464 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400465 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400466 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400467 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400468 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400469 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400470 ctx := a.cfg.Context()
471 d := ctx.CompileBytes(buf.Bytes())
472 res := a.cfg.Unify(d).Eval()
473 if err := res.Err(); err != nil {
giob7df27f2026-07-28 10:36:17 +0400474 return rendered{}, errors.New(cueerrors.Details(err, nil))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400475 }
476 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400477 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400478 }
gio308105e2024-04-19 13:12:13 +0400479 full, err := json.MarshalIndent(res, "", "\t")
480 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400481 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400482 }
gio94904702024-07-26 16:58:34 +0400483 ret.Raw = full
gio308105e2024-04-19 13:12:13 +0400484 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400485 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
486 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400487 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400488 }
489 ret.Readme = readme
giof6ad2982024-08-23 17:42:49 +0400490 res.LookupPath(cue.ParsePath("input.cluster.name")).Decode(&ret.Cluster)
491 if err := res.LookupPath(cue.ParsePath("output.clusterProxy")).Decode(&ret.ClusterProxies); err != nil {
492 return rendered{}, err
493 }
494 if err := res.LookupPath(cue.ParsePath("namespaces")).Decode(&ret.Namespaces); err != nil {
495 return rendered{}, err
496 }
gio802311e2024-11-04 08:37:34 +0400497 if err := res.LookupPath(cue.ParsePath("output.openPort")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400498 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400499 }
gio0eaf2712024-04-14 13:08:46 +0400500 {
gio212f8002025-07-08 14:28:43 +0400501 envVars := []string{}
502 if err := res.LookupPath(cue.ParsePath("envVars")).Decode(&envVars); err != nil {
503 return rendered{}, err
504 }
505 for _, ev := range envVars {
506 items := strings.SplitN(ev, "=", 2)
507 if len(items) != 2 {
508 panic(ev)
509 }
510 ret.EnvVars = append(ret.EnvVars, EnvVar{items[0], items[1]})
511 }
512 }
513 {
gio7fbd4ad2024-08-27 10:06:39 +0400514 charts := res.LookupPath(cue.ParsePath("output.charts"))
giof8843412024-05-22 16:38:05 +0400515 i, err := charts.Fields()
516 if err != nil {
517 return rendered{}, err
518 }
519 for i.Next() {
520 var chartRef helmChartRef
521 if err := i.Value().Decode(&chartRef); err != nil {
522 return rendered{}, err
523 }
524 if chartRef.Kind == "GitRepository" {
525 var chart HelmChartGitRepo
526 if err := i.Value().Decode(&chart); err != nil {
527 return rendered{}, err
528 }
529 ret.HelmCharts.Git[cleanName(i.Selector().String())] = chart
530 }
531 }
532 }
533 {
gio7fbd4ad2024-08-27 10:06:39 +0400534 images := res.LookupPath(cue.ParsePath("output.images"))
giof8843412024-05-22 16:38:05 +0400535 i, err := images.Fields()
536 if err != nil {
537 return rendered{}, err
538 }
539 for i.Next() {
540 var img ContainerImage
541 if err := i.Value().Decode(&img); err != nil {
542 return rendered{}, err
543 }
544 ret.ContainerImages[cleanName(i.Selector().String())] = img
545 }
546 }
547 {
gio7fbd4ad2024-08-27 10:06:39 +0400548 helm := res.LookupPath(cue.ParsePath("output.helm"))
549 i, err := helm.Fields()
gio0eaf2712024-04-14 13:08:46 +0400550 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400551 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400552 }
553 for i.Next() {
554 if contents, err := cueyaml.Encode(i.Value()); err != nil {
555 return rendered{}, err
556 } else {
557 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
558 ret.Resources[name] = contents
559 }
560 }
561 }
562 {
563 resources := res.LookupPath(cue.ParsePath("resources"))
564 i, err := resources.Fields()
565 if err != nil {
566 return rendered{}, err
567 }
568 for i.Next() {
569 if contents, err := cueyaml.Encode(i.Value()); err != nil {
570 return rendered{}, err
571 } else {
572 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
573 ret.Resources[name] = contents
574 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400575 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400576 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400577 helpValue := res.LookupPath(cue.ParsePath("help"))
578 if helpValue.Exists() {
579 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400580 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400581 }
582 }
583 url, err := res.LookupPath(cue.ParsePath("url")).String()
584 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400585 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400586 }
gio09a3e5b2024-04-26 14:11:06 +0400587 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400588 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
589 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400590 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400591 }
592 ret.Icon = icon
gio6ce44812025-05-17 07:31:54 +0400593 access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
594 if err != nil {
595 return rendered{}, err
596 }
597 ret.Access = access
598 return ret, nil
599}
600
601func extractAccessInternal(v cue.Value) ([]Access, error) {
602 ret := []Access{}
603 a := v.LookupPath(cue.ParsePath("access"))
604 if err := a.Err(); err != nil {
605 return nil, err
606 }
607 i, err := a.List()
608 if err != nil {
609 return nil, err
610 }
611 for i.Next() {
612 n := i.Value().LookupPath(cue.ParsePath("name"))
613 if err := n.Err(); err != nil {
614 return nil, err
615 }
616 nn, err := n.String()
617 if err != nil {
618 return nil, err
619 }
620 t := i.Value().LookupPath(cue.ParsePath("type"))
621 if err := t.Err(); err != nil {
622 return nil, err
623 }
624 d, err := t.String()
625 if err != nil {
626 return nil, err
627 }
628 switch d {
629 case "https":
630 {
631 var q AccessHTTPS
632 if err := i.Value().Decode(&q); err != nil {
633 return nil, err
634 }
635 ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
636 }
637 case "ssh":
638 {
639 var q AccessSSH
640 if err := i.Value().Decode(&q); err != nil {
641 return nil, err
642 }
643 ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
644 }
645 case "tcp":
646 {
647 var q AccessTCP
648 if err := i.Value().Decode(&q); err != nil {
649 return nil, err
650 }
651 ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
652 }
653 case "udp":
654 {
655 var q AccessUDP
656 if err := i.Value().Decode(&q); err != nil {
657 return nil, err
658 }
659 ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
660 }
661 case "postgresql":
662 {
663 var q AccessPostgreSQL
664 if err := i.Value().Decode(&q); err != nil {
665 return nil, err
666 }
667 ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
668 }
669 case "mongodb":
670 {
671 var q AccessMongoDB
672 if err := i.Value().Decode(&q); err != nil {
673 return nil, err
674 }
675 ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
676 }
gio33222472025-08-04 12:02:34 +0400677 case "env_var":
678 {
679 var q AccessEnvVar
680 if err := i.Value().Decode(&q); err != nil {
681 return nil, err
682 }
683 ret = append(ret, Access{Type: "env_var", Name: nn, EnvVar: &q})
684 }
gio6ce44812025-05-17 07:31:54 +0400685 }
686 }
687 for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
688 subout := v.LookupPath(cue.ParsePath(sub))
689 if subout.Err() != nil {
690 continue
691 }
692 if a, err := extractAccess(subout); err != nil {
693 return nil, err
694 } else {
695 ret = append(ret, a...)
696 }
697 }
698 return ret, nil
699}
700
701func extractAccess(v cue.Value) ([]Access, error) {
702 if err := v.Err(); err != nil {
703 return nil, err
704 }
705 i, err := v.Fields()
706 if err != nil {
707 return nil, err
708 }
709 ret := []Access{}
710 for i.Next() {
711 if a, err := extractAccessInternal(i.Value()); err != nil {
giob7df27f2026-07-28 10:36:17 +0400712 return nil, errors.New(cueerrors.Details(err, nil))
gio6ce44812025-05-17 07:31:54 +0400713 } else {
714 ret = append(ret, a...)
715 }
716 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400717 return ret, nil
718}
719
gio3cdee592024-04-17 10:15:56 +0400720type cueEnvApp struct {
721 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400722}
723
gio308105e2024-04-19 13:12:13 +0400724func NewCueEnvApp(data CueAppData) (EnvApp, error) {
725 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400726 if err != nil {
727 return nil, err
728 }
gio3cdee592024-04-17 10:15:56 +0400729 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400730}
731
gio0eaf2712024-04-14 13:08:46 +0400732func NewDodoApp(appCfg []byte) (EnvApp, error) {
733 return NewCueEnvApp(CueAppData{
giofc9c4ea2024-06-26 13:46:53 +0400734 "app.cue": appCfg,
gio33222472025-08-04 12:02:34 +0400735 "base.cue": cueBaseConfig,
giofc9c4ea2024-06-26 13:46:53 +0400736 "dodo.cue": dodoAppCue,
gio33222472025-08-04 12:02:34 +0400737 "env.cue": cueEnvAppGlobal,
gio0eaf2712024-04-14 13:08:46 +0400738 })
739}
740
gio3cdee592024-04-17 10:15:56 +0400741func (a cueEnvApp) Type() AppType {
742 return AppTypeEnv
743}
744
giofc441e32024-11-11 16:26:14 +0400745func merge(d map[string]any, v map[string]any) map[string]any {
746 ret := map[string]any{}
747 for k, val := range d {
748 if vv, ok := v[k]; ok && vv != nil {
749 if mv, ok := val.(map[string]any); ok {
750 // TODO(gio): check that it is actually map
gio28356152025-07-24 17:20:56 +0400751 mm, ok := vv.(map[string]any)
752 if !ok {
753 // TODO(gio): handle #Network and others
754 ret[k] = vv
755 } else {
756 ret[k] = merge(mv, mm)
757 }
giofc441e32024-11-11 16:26:14 +0400758 } else {
759 ret[k] = vv
760 }
761 } else {
762 ret[k] = val
763 }
764 }
765 for k, v := range v {
766 if v != nil {
767 if _, ok := d[k]; !ok {
gio838bcb82025-05-15 19:39:04 +0400768 ret[k] = v
giofc441e32024-11-11 16:26:14 +0400769 }
770 }
771 }
772 return ret
773}
774
giocb34ad22024-07-11 08:01:13 +0400775func (a cueEnvApp) Render(
776 release Release,
777 env EnvConfig,
778 networks []Network,
giof6ad2982024-08-23 17:42:49 +0400779 clusters []Cluster,
giocb34ad22024-07-11 08:01:13 +0400780 values map[string]any,
781 charts map[string]helmv2.HelmChartTemplateSpec,
gio864b4332024-09-05 13:56:47 +0400782 vpnKeyGen VPNAPIClient,
giocb34ad22024-07-11 08:01:13 +0400783) (EnvAppRendered, error) {
giofc441e32024-11-11 16:26:14 +0400784 dv, err := ExtractDefaultValues(a.cueApp.cfg.LookupPath(cue.ParsePath("input")))
785 if err != nil {
786 return EnvAppRendered{}, err
787 }
gio842db3f2025-05-30 11:57:20 +0400788 if dv == nil {
789 dv = map[string]any{}
790 }
giofc441e32024-11-11 16:26:14 +0400791 mv := merge(dv.(map[string]any), values)
gio842db3f2025-05-30 11:57:20 +0400792 derived, err := deriveValues(mv, mv, a.Schema(), networks, clusters, vpnKeyGen)
gio3cdee592024-04-17 10:15:56 +0400793 if err != nil {
gioefa0ed42024-06-13 12:31:43 +0400794 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400795 }
giof8843412024-05-22 16:38:05 +0400796 if charts == nil {
797 charts = make(map[string]helmv2.HelmChartTemplateSpec)
798 }
giof15b9da2024-09-19 06:59:16 +0400799 if clusters == nil {
800 clusters = []Cluster{}
801 }
gio3cdee592024-04-17 10:15:56 +0400802 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400803 "global": env,
804 "release": release,
805 "input": derived,
806 "localCharts": charts,
gio5e49bb62024-07-20 10:43:19 +0400807 "networks": NetworkMap(networks),
giof15b9da2024-09-19 06:59:16 +0400808 "clusters": clusters,
gio3cdee592024-04-17 10:15:56 +0400809 })
810 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400811 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400812 }
gioe72b54f2024-04-22 10:44:41 +0400813 return EnvAppRendered{
814 rendered: ret,
815 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400816 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400817 Env: env,
818 Release: release,
819 Values: values,
820 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400821 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400822 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400823 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400824 },
825 }, nil
gio3cdee592024-04-17 10:15:56 +0400826}
827
828type cueInfraApp struct {
829 cueApp
830}
831
gio308105e2024-04-19 13:12:13 +0400832func NewCueInfraApp(data CueAppData) (InfraApp, error) {
833 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400834 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400835 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400836 }
gio3cdee592024-04-17 10:15:56 +0400837 return cueInfraApp{app}, nil
838}
839
840func (a cueInfraApp) Type() AppType {
841 return AppTypeInfra
842}
843
gio7841f4f2024-07-26 19:53:49 +0400844func (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 +0400845 if charts == nil {
846 charts = make(map[string]helmv2.HelmChartTemplateSpec)
847 }
gioe72b54f2024-04-22 10:44:41 +0400848 ret, err := a.cueApp.render(map[string]any{
giof8843412024-05-22 16:38:05 +0400849 "global": infra,
850 "release": release,
851 "input": values,
852 "localCharts": charts,
gio7841f4f2024-07-26 19:53:49 +0400853 "networks": InfraNetworkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400854 })
gioe72b54f2024-04-22 10:44:41 +0400855 if err != nil {
856 return InfraAppRendered{}, err
857 }
858 return InfraAppRendered{
859 rendered: ret,
860 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400861 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400862 Infra: infra,
863 Release: release,
864 Values: values,
865 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400866 URL: ret.URL,
867 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400868 },
869 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400870}
871
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400872func cleanName(s string) string {
873 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400874}
gioe72b54f2024-04-22 10:44:41 +0400875
876func join[T fmt.Stringer](items []T, sep string) string {
877 var tmp []string
878 for _, i := range items {
879 tmp = append(tmp, i.String())
880 }
881 return strings.Join(tmp, ",")
882}
gio0eaf2712024-04-14 13:08:46 +0400883
gio5e49bb62024-07-20 10:43:19 +0400884func NetworkMap(networks []Network) map[string]Network {
gio0eaf2712024-04-14 13:08:46 +0400885 ret := make(map[string]Network)
886 for _, n := range networks {
887 ret[strings.ToLower(n.Name)] = n
888 }
889 return ret
890}
gio7841f4f2024-07-26 19:53:49 +0400891
892func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
893 ret := make(map[string]InfraNetwork)
894 for _, n := range networks {
895 ret[strings.ToLower(n.Name)] = n
896 }
897 return ret
898}