blob: 1626a1a04f784cc7d74ba1f0501d9b4277908d5a [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"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +04005 "encoding/json"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +04006 "fmt"
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +04007 template "html/template"
gio3cdee592024-04-17 10:15:56 +04008 "net"
gioe72b54f2024-04-22 10:44:41 +04009 "net/netip"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040010 "strings"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040011
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040012 "cuelang.org/go/cue"
gio308105e2024-04-19 13:12:13 +040013 "cuelang.org/go/cue/build"
14 "cuelang.org/go/cue/cuecontext"
15 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040016 cueyaml "cuelang.org/go/encoding/yaml"
giolekva8aa73e82022-07-09 11:34:39 +040017)
giolekva050609f2021-12-29 15:51:40 +040018
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040019// TODO(gio): import
gioe72b54f2024-04-22 10:44:41 +040020const cueEnvAppGlobal = `
21#Global: {
22 id: string | *""
23 pcloudEnvName: string | *""
24 domain: string | *""
25 privateDomain: string | *""
26 contactEmail: string | *""
27 adminPublicKey: string | *""
28 publicIP: [...string] | *[]
29 nameserverIP: [...string] | *[]
30 namespacePrefix: string | *""
31 network: #EnvNetwork
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040032}
33
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040034networks: {
35 public: #Network & {
36 name: "Public"
37 ingressClass: "\(global.pcloudEnvName)-ingress-public"
38 certificateIssuer: "\(global.id)-public"
39 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040040 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040041 }
42 private: #Network & {
43 name: "Private"
44 ingressClass: "\(global.id)-ingress-private"
45 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040046 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040047 }
48}
49
gioe72b54f2024-04-22 10:44:41 +040050// TODO(gio): remove
51ingressPrivate: "\(global.id)-ingress-private"
52ingressPublic: "\(global.pcloudEnvName)-ingress-public"
53issuerPrivate: "\(global.id)-private"
54issuerPublic: "\(global.id)-public"
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040055
gio1de49582024-04-21 08:33:57 +040056#Ingress: {
57 auth: #Auth
58 network: #Network
59 subdomain: string
60 service: close({
61 name: string
62 port: close({ name: string }) | close({ number: int & > 0 })
63 })
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040064
gio1de49582024-04-21 08:33:57 +040065 _domain: "\(subdomain).\(network.domain)"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040066 _authProxyHTTPPortName: "http"
67
68 out: {
69 images: {
70 authProxy: #Image & {
71 repository: "giolekva"
72 name: "auth-proxy"
73 tag: "latest"
74 pullPolicy: "Always"
75 }
76 }
77 charts: {
78 ingress: #Chart & {
79 chart: "charts/ingress"
80 sourceRef: {
81 kind: "GitRepository"
82 name: "pcloud"
83 namespace: global.id
84 }
85 }
86 authProxy: #Chart & {
87 chart: "charts/auth-proxy"
88 sourceRef: {
89 kind: "GitRepository"
90 name: "pcloud"
91 namespace: global.id
92 }
93 }
94 }
95 helm: {
gio1de49582024-04-21 08:33:57 +040096 if auth.enabled {
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040097 "auth-proxy": {
98 chart: charts.authProxy
99 values: {
100 image: {
101 repository: images.authProxy.fullName
102 tag: images.authProxy.tag
103 pullPolicy: images.authProxy.pullPolicy
104 }
gio1de49582024-04-21 08:33:57 +0400105 upstream: "\(service.name).\(release.namespace).svc.cluster.local"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400106 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
107 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400108 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
gio1de49582024-04-21 08:33:57 +0400109 groups: auth.groups
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400110 portName: _authProxyHTTPPortName
111 }
112 }
113 }
114 ingress: {
115 chart: charts.ingress
gio1de49582024-04-21 08:33:57 +0400116 _service: service
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400117 values: {
118 domain: _domain
gio1de49582024-04-21 08:33:57 +0400119 ingressClassName: network.ingressClass
120 certificateIssuer: network.certificateIssuer
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400121 service: {
gio1de49582024-04-21 08:33:57 +0400122 if auth.enabled {
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400123 name: "auth-proxy"
124 port: name: _authProxyHTTPPortName
125 }
gio1de49582024-04-21 08:33:57 +0400126 if !auth.enabled {
127 name: _service.name
128 if _service.port.name != _|_ {
129 port: name: _service.port.name
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400130 }
gio1de49582024-04-21 08:33:57 +0400131 if _service.port.number != _|_ {
132 port: number: _service.port.number
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400133 }
134 }
135 }
136 }
137 }
138 }
139 }
140}
141
gio1de49582024-04-21 08:33:57 +0400142ingress: {}
143
144_ingressValidate: {
145 for key, value in ingress {
146 "\(key)": #Ingress & value
147 }
148}
gioe72b54f2024-04-22 10:44:41 +0400149`
150
151const cueInfraAppGlobal = `
152#Global: {
153 pcloudEnvName: string | *""
154 publicIP: [...string] | *[]
155 namespacePrefix: string | *""
156 infraAdminPublicKey: string | *""
157}
158
159// TODO(gio): remove
160ingressPublic: "\(global.pcloudEnvName)-ingress-public"
161
162ingress: {}
163_ingressValidate: {}
164`
165
166const cueBaseConfig = `
167import (
168 "net"
169)
170
171name: string | *""
172description: string | *""
173readme: string | *""
174icon: string | *""
175namespace: string | *""
176
177help: [...#HelpDocument] | *[]
178
179#HelpDocument: {
180 title: string
181 contents: string
182 children: [...#HelpDocument] | *[]
183}
184
185url: string | *""
186
187#AppType: "infra" | "env"
188appType: #AppType | *"env"
189
190#Auth: {
191 enabled: bool | *false // TODO(gio): enabled by default?
192 groups: string | *"" // TODO(gio): []string
193}
194
195#Network: {
196 name: string
197 ingressClass: string
198 certificateIssuer: string | *""
199 domain: string
200 allocatePortAddr: string
201}
202
203#Image: {
204 registry: string | *"docker.io"
205 repository: string
206 name: string
207 tag: string
208 pullPolicy: string | *"IfNotPresent"
209 imageName: "\(repository)/\(name)"
210 fullName: "\(registry)/\(imageName)"
211 fullNameWithTag: "\(fullName):\(tag)"
212}
213
214#Chart: {
215 chart: string
216 sourceRef: #SourceRef
217}
218
219#SourceRef: {
220 kind: "GitRepository" | "HelmRepository"
221 name: string
222 namespace: string // TODO(gio): default global.id
223}
224
225#EnvNetwork: {
226 dns: net.IPv4
227 dnsInClusterIP: net.IPv4
228 ingress: net.IPv4
229 headscale: net.IPv4
230 servicesFrom: net.IPv4
231 servicesTo: net.IPv4
232}
233
234#Release: {
235 appInstanceId: string
236 namespace: string
237 repoAddr: string
238 appDir: string
239}
240
241#PortForward: {
242 allocator: string
243 protocol: "TCP" | "UDP" | *"TCP"
244 sourcePort: int
245 targetService: string
246 targetPort: int
247}
248
249portForward: [...#PortForward] | *[]
250
251global: #Global
252release: #Release
gio1de49582024-04-21 08:33:57 +0400253
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400254images: {
255 for key, value in images {
256 "\(key)": #Image & value
257 }
gio1de49582024-04-21 08:33:57 +0400258 for _, value in _ingressValidate {
259 for name, image in value.out.images {
260 "\(name)": #Image & image
261 }
262 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400263}
264
265charts: {
266 for key, value in charts {
267 "\(key)": #Chart & value
268 }
gio1de49582024-04-21 08:33:57 +0400269 for _, value in _ingressValidate {
270 for name, chart in value.out.charts {
271 "\(name)": #Chart & chart
272 }
273 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400274}
275
276#ResourceReference: {
277 name: string
278 namespace: string
279}
280
281#Helm: {
282 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400283 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400284 ...
285}
286
gio1de49582024-04-21 08:33:57 +0400287_helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400288 for key, value in helm {
289 "\(key)": #Helm & value & {
290 name: key
291 }
292 }
gio1de49582024-04-21 08:33:57 +0400293 for key, value in _ingressValidate {
294 for ing, ingValue in value.out.helm {
295 // TODO(gio): support multiple ingresses
296 // "\(key)-\(ing)": #Helm & ingValue & {
297 "\(ing)": #Helm & ingValue & {
298 // name: "\(key)-\(ing)"
299 name: ing
300 }
301 }
302 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400303}
304
305#HelmRelease: {
306 _name: string
307 _chart: #Chart
308 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400309 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400310
311 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
312 kind: "HelmRelease"
313 metadata: {
314 name: _name
315 namespace: release.namespace
316 }
317 spec: {
318 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400319 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400320 chart: {
321 spec: _chart
322 }
323 values: _values
324 }
325}
326
327output: {
gio1de49582024-04-21 08:33:57 +0400328 for name, r in _helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400329 "\(name)": #HelmRelease & {
330 _name: name
331 _chart: r.chart
332 _values: r.values
333 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400334 }
335 }
336}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400337
338#SSHKey: {
339 public: string
340 private: string
341}
gio09a3e5b2024-04-26 14:11:06 +0400342
343#HelpDocument: {
344 title: string
345 contents: string
346 children: [...#HelpDocument]
347}
348
349help: [...#HelpDocument] | *[]
350
351url: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400352`
353
gioe72b54f2024-04-22 10:44:41 +0400354type rendered struct {
gio3cdee592024-04-17 10:15:56 +0400355 Name string
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400356 Readme string
gio308105e2024-04-19 13:12:13 +0400357 Resources CueAppData
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400358 Ports []PortForward
gio308105e2024-04-19 13:12:13 +0400359 Data CueAppData
gio09a3e5b2024-04-26 14:11:06 +0400360 URL string
Davit Tabidze56f86a42024-04-09 19:15:25 +0400361 Help []HelpDocument
Davit Tabidze56f86a42024-04-09 19:15:25 +0400362 Icon string
363}
364
365type HelpDocument struct {
366 Title string
367 Contents string
368 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400369}
370
gioe72b54f2024-04-22 10:44:41 +0400371type EnvAppRendered struct {
372 rendered
373 Config AppInstanceConfig
374}
375
376type InfraAppRendered struct {
377 rendered
378 Config InfraAppInstanceConfig
379}
380
gio3cdee592024-04-17 10:15:56 +0400381type PortForward struct {
382 Allocator string `json:"allocator"`
383 Protocol string `json:"protocol"`
384 SourcePort int `json:"sourcePort"`
385 TargetService string `json:"targetService"`
386 TargetPort int `json:"targetPort"`
387}
388
389type AppType int
390
391const (
392 AppTypeInfra AppType = iota
393 AppTypeEnv
394)
395
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400396type App interface {
397 Name() string
gio44f621b2024-04-29 09:44:38 +0400398 Type() AppType
399 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400400 Description() string
401 Icon() template.HTML
402 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400403 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400404}
405
406type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400407 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
408 PublicIP []net.IP `json:"publicIP,omitempty"`
409 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
410 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400411}
412
413type InfraApp interface {
414 App
gioe72b54f2024-04-22 10:44:41 +0400415 Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error)
416}
417
418type EnvNetwork struct {
419 DNS net.IP `json:"dns,omitempty"`
420 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
421 Ingress net.IP `json:"ingress,omitempty"`
422 Headscale net.IP `json:"headscale,omitempty"`
423 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
424 ServicesTo net.IP `json:"servicesTo,omitempty"`
425}
426
427func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
428 addr, err := netip.ParseAddr(subnet.String())
429 if err != nil {
430 return EnvNetwork{}, err
431 }
432 if !addr.Is4() {
433 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
434 }
435 dns := addr.Next()
436 ingress := dns.Next()
437 headscale := ingress.Next()
438 b := addr.AsSlice()
439 if b[3] != 0 {
440 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
441 }
442 b[3] = 10
443 servicesFrom, ok := netip.AddrFromSlice(b)
444 if !ok {
445 return EnvNetwork{}, fmt.Errorf("Must not reach")
446 }
447 b[3] = 254
448 servicesTo, ok := netip.AddrFromSlice(b)
449 if !ok {
450 return EnvNetwork{}, fmt.Errorf("Must not reach")
451 }
452 b[3] = b[2]
453 b[2] = b[1]
454 b[0] = 10
455 b[1] = 44
456 dnsInClusterIP, ok := netip.AddrFromSlice(b)
457 if !ok {
458 return EnvNetwork{}, fmt.Errorf("Must not reach")
459 }
460 return EnvNetwork{
461 DNS: net.ParseIP(dns.String()),
462 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
463 Ingress: net.ParseIP(ingress.String()),
464 Headscale: net.ParseIP(headscale.String()),
465 ServicesFrom: net.ParseIP(servicesFrom.String()),
466 ServicesTo: net.ParseIP(servicesTo.String()),
467 }, nil
gio3cdee592024-04-17 10:15:56 +0400468}
469
470// TODO(gio): rename to EnvConfig
gioe72b54f2024-04-22 10:44:41 +0400471type EnvConfig struct {
472 Id string `json:"id,omitempty"`
473 InfraName string `json:"pcloudEnvName,omitempty"`
474 Domain string `json:"domain,omitempty"`
475 PrivateDomain string `json:"privateDomain,omitempty"`
476 ContactEmail string `json:"contactEmail,omitempty"`
477 AdminPublicKey string `json:"adminPublicKey,omitempty"`
478 PublicIP []net.IP `json:"publicIP,omitempty"`
479 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
480 NamespacePrefix string `json:"namespacePrefix,omitempty"`
481 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400482}
483
484type EnvApp interface {
485 App
gioe72b54f2024-04-22 10:44:41 +0400486 Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400487}
488
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400489type cueApp struct {
490 name string
491 description string
492 icon template.HTML
493 namespace string
494 schema Schema
gio308105e2024-04-19 13:12:13 +0400495 cfg cue.Value
496 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400497}
498
gio308105e2024-04-19 13:12:13 +0400499type CueAppData map[string][]byte
500
501func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
502 ctx := cuecontext.New()
503 buildCtx := build.NewContext()
504 cfg := &load.Config{
505 Context: buildCtx,
506 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400507 }
gio308105e2024-04-19 13:12:13 +0400508 names := make([]string, 0)
509 for n, b := range data {
510 a := fmt.Sprintf("/%s", n)
511 names = append(names, a)
512 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
513 }
514 instances := load.Instances(names, cfg)
515 for _, inst := range instances {
516 if inst.Err != nil {
517 return cue.Value{}, inst.Err
518 }
519 }
520 if len(instances) != 1 {
521 return cue.Value{}, fmt.Errorf("invalid")
522 }
523 ret := ctx.BuildInstance(instances[0])
524 if ret.Err() != nil {
525 return cue.Value{}, ret.Err()
526 }
527 if err := ret.Validate(); err != nil {
528 return cue.Value{}, err
529 }
530 return ret, nil
531}
532
533func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400534 cfg := struct {
535 Name string `json:"name"`
536 Namespace string `json:"namespace"`
537 Description string `json:"description"`
538 Icon string `json:"icon"`
539 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400540 if err := config.Decode(&cfg); err != nil {
541 return cueApp{}, err
542 }
gio44f621b2024-04-29 09:44:38 +0400543 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400544 if err != nil {
545 return cueApp{}, err
546 }
547 return cueApp{
548 name: cfg.Name,
549 description: cfg.Description,
550 icon: template.HTML(cfg.Icon),
551 namespace: cfg.Namespace,
552 schema: schema,
553 cfg: config,
gio308105e2024-04-19 13:12:13 +0400554 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400555 }, nil
556}
557
gio308105e2024-04-19 13:12:13 +0400558func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
559 config, err := ParseCueAppConfig(data)
560 if err != nil {
561 return cueApp{}, err
562 }
563 return newCueApp(config, data)
564}
565
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400566func (a cueApp) Name() string {
567 return a.name
568}
569
gio44f621b2024-04-29 09:44:38 +0400570func (a cueApp) Slug() string {
571 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
572}
573
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400574func (a cueApp) Description() string {
575 return a.description
576}
577
578func (a cueApp) Icon() template.HTML {
579 return a.icon
580}
581
582func (a cueApp) Schema() Schema {
583 return a.schema
584}
585
gioef01fbb2024-04-12 16:52:59 +0400586func (a cueApp) Namespace() string {
587 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400588}
589
gioe72b54f2024-04-22 10:44:41 +0400590func (a cueApp) render(values map[string]any) (rendered, error) {
591 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400592 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400593 Resources: make(CueAppData),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400594 Ports: make([]PortForward, 0),
gio308105e2024-04-19 13:12:13 +0400595 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400596 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400597 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400598 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400599 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400600 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400601 ctx := a.cfg.Context()
602 d := ctx.CompileBytes(buf.Bytes())
603 res := a.cfg.Unify(d).Eval()
604 if err := res.Err(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400605 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400606 }
607 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400608 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400609 }
gio308105e2024-04-19 13:12:13 +0400610 full, err := json.MarshalIndent(res, "", "\t")
611 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400612 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400613 }
614 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400615 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
616 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400617 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400618 }
619 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400620 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400621 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400622 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400623 output := res.LookupPath(cue.ParsePath("output"))
624 i, err := output.Fields()
625 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400626 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400627 }
628 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400629 if contents, err := cueyaml.Encode(i.Value()); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400630 return rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400631 } else {
632 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
633 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400634 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400635 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400636 helpValue := res.LookupPath(cue.ParsePath("help"))
637 if helpValue.Exists() {
638 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400639 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400640 }
641 }
642 url, err := res.LookupPath(cue.ParsePath("url")).String()
643 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400644 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400645 }
gio09a3e5b2024-04-26 14:11:06 +0400646 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400647 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
648 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400649 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400650 }
651 ret.Icon = icon
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400652 return ret, nil
653}
654
gio3cdee592024-04-17 10:15:56 +0400655type cueEnvApp struct {
656 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400657}
658
gio308105e2024-04-19 13:12:13 +0400659func NewCueEnvApp(data CueAppData) (EnvApp, error) {
660 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400661 if err != nil {
662 return nil, err
663 }
gio3cdee592024-04-17 10:15:56 +0400664 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400665}
666
gio3cdee592024-04-17 10:15:56 +0400667func (a cueEnvApp) Type() AppType {
668 return AppTypeEnv
669}
670
gioe72b54f2024-04-22 10:44:41 +0400671func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error) {
gio3cdee592024-04-17 10:15:56 +0400672 networks := CreateNetworks(env)
673 derived, err := deriveValues(values, a.Schema(), networks)
674 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400675 return EnvAppRendered{}, nil
gio3cdee592024-04-17 10:15:56 +0400676 }
677 ret, err := a.cueApp.render(map[string]any{
678 "global": env,
679 "release": release,
680 "input": derived,
681 })
682 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400683 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400684 }
gioe72b54f2024-04-22 10:44:41 +0400685 return EnvAppRendered{
686 rendered: ret,
687 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400688 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400689 Env: env,
690 Release: release,
691 Values: values,
692 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400693 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400694 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400695 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400696 },
697 }, nil
gio3cdee592024-04-17 10:15:56 +0400698}
699
700type cueInfraApp struct {
701 cueApp
702}
703
gio308105e2024-04-19 13:12:13 +0400704func NewCueInfraApp(data CueAppData) (InfraApp, error) {
705 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400706 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400707 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400708 }
gio3cdee592024-04-17 10:15:56 +0400709 return cueInfraApp{app}, nil
710}
711
712func (a cueInfraApp) Type() AppType {
713 return AppTypeInfra
714}
715
gioe72b54f2024-04-22 10:44:41 +0400716func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error) {
717 ret, err := a.cueApp.render(map[string]any{
gio3cdee592024-04-17 10:15:56 +0400718 "global": infra,
719 "release": release,
720 "input": values,
721 })
gioe72b54f2024-04-22 10:44:41 +0400722 if err != nil {
723 return InfraAppRendered{}, err
724 }
725 return InfraAppRendered{
726 rendered: ret,
727 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400728 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400729 Infra: infra,
730 Release: release,
731 Values: values,
732 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400733 URL: ret.URL,
734 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400735 },
736 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400737}
738
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400739func cleanName(s string) string {
740 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400741}
gioe72b54f2024-04-22 10:44:41 +0400742
743func join[T fmt.Stringer](items []T, sep string) string {
744 var tmp []string
745 for _, i := range items {
746 tmp = append(tmp, i.String())
747 }
748 return strings.Join(tmp, ",")
749}