blob: 11ca8cd859bd807b2f6e507cfa83ab2024137a91 [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}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400342`
343
gioe72b54f2024-04-22 10:44:41 +0400344type rendered struct {
gio3cdee592024-04-17 10:15:56 +0400345 Name string
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400346 Readme string
gio308105e2024-04-19 13:12:13 +0400347 Resources CueAppData
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400348 Ports []PortForward
gio308105e2024-04-19 13:12:13 +0400349 Data CueAppData
Davit Tabidze56f86a42024-04-09 19:15:25 +0400350 Help []HelpDocument
351 Url string
352 Icon string
353}
354
355type HelpDocument struct {
356 Title string
357 Contents string
358 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400359}
360
gioe72b54f2024-04-22 10:44:41 +0400361type EnvAppRendered struct {
362 rendered
363 Config AppInstanceConfig
364}
365
366type InfraAppRendered struct {
367 rendered
368 Config InfraAppInstanceConfig
369}
370
gio3cdee592024-04-17 10:15:56 +0400371type PortForward struct {
372 Allocator string `json:"allocator"`
373 Protocol string `json:"protocol"`
374 SourcePort int `json:"sourcePort"`
375 TargetService string `json:"targetService"`
376 TargetPort int `json:"targetPort"`
377}
378
379type AppType int
380
381const (
382 AppTypeInfra AppType = iota
383 AppTypeEnv
384)
385
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400386type App interface {
387 Name() string
gio44f621b2024-04-29 09:44:38 +0400388 Type() AppType
389 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400390 Description() string
391 Icon() template.HTML
392 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400393 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400394}
395
396type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400397 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
398 PublicIP []net.IP `json:"publicIP,omitempty"`
399 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
400 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400401}
402
403type InfraApp interface {
404 App
gioe72b54f2024-04-22 10:44:41 +0400405 Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error)
406}
407
408type EnvNetwork struct {
409 DNS net.IP `json:"dns,omitempty"`
410 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
411 Ingress net.IP `json:"ingress,omitempty"`
412 Headscale net.IP `json:"headscale,omitempty"`
413 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
414 ServicesTo net.IP `json:"servicesTo,omitempty"`
415}
416
417func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
418 addr, err := netip.ParseAddr(subnet.String())
419 if err != nil {
420 return EnvNetwork{}, err
421 }
422 if !addr.Is4() {
423 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
424 }
425 dns := addr.Next()
426 ingress := dns.Next()
427 headscale := ingress.Next()
428 b := addr.AsSlice()
429 if b[3] != 0 {
430 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
431 }
432 b[3] = 10
433 servicesFrom, ok := netip.AddrFromSlice(b)
434 if !ok {
435 return EnvNetwork{}, fmt.Errorf("Must not reach")
436 }
437 b[3] = 254
438 servicesTo, ok := netip.AddrFromSlice(b)
439 if !ok {
440 return EnvNetwork{}, fmt.Errorf("Must not reach")
441 }
442 b[3] = b[2]
443 b[2] = b[1]
444 b[0] = 10
445 b[1] = 44
446 dnsInClusterIP, ok := netip.AddrFromSlice(b)
447 if !ok {
448 return EnvNetwork{}, fmt.Errorf("Must not reach")
449 }
450 return EnvNetwork{
451 DNS: net.ParseIP(dns.String()),
452 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
453 Ingress: net.ParseIP(ingress.String()),
454 Headscale: net.ParseIP(headscale.String()),
455 ServicesFrom: net.ParseIP(servicesFrom.String()),
456 ServicesTo: net.ParseIP(servicesTo.String()),
457 }, nil
gio3cdee592024-04-17 10:15:56 +0400458}
459
460// TODO(gio): rename to EnvConfig
gioe72b54f2024-04-22 10:44:41 +0400461type EnvConfig struct {
462 Id string `json:"id,omitempty"`
463 InfraName string `json:"pcloudEnvName,omitempty"`
464 Domain string `json:"domain,omitempty"`
465 PrivateDomain string `json:"privateDomain,omitempty"`
466 ContactEmail string `json:"contactEmail,omitempty"`
467 AdminPublicKey string `json:"adminPublicKey,omitempty"`
468 PublicIP []net.IP `json:"publicIP,omitempty"`
469 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
470 NamespacePrefix string `json:"namespacePrefix,omitempty"`
471 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400472}
473
474type EnvApp interface {
475 App
gioe72b54f2024-04-22 10:44:41 +0400476 Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400477}
478
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400479type cueApp struct {
480 name string
481 description string
482 icon template.HTML
483 namespace string
484 schema Schema
gio308105e2024-04-19 13:12:13 +0400485 cfg cue.Value
486 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400487}
488
gio308105e2024-04-19 13:12:13 +0400489type CueAppData map[string][]byte
490
491func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
492 ctx := cuecontext.New()
493 buildCtx := build.NewContext()
494 cfg := &load.Config{
495 Context: buildCtx,
496 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400497 }
gio308105e2024-04-19 13:12:13 +0400498 names := make([]string, 0)
499 for n, b := range data {
500 a := fmt.Sprintf("/%s", n)
501 names = append(names, a)
502 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
503 }
504 instances := load.Instances(names, cfg)
505 for _, inst := range instances {
506 if inst.Err != nil {
507 return cue.Value{}, inst.Err
508 }
509 }
510 if len(instances) != 1 {
511 return cue.Value{}, fmt.Errorf("invalid")
512 }
513 ret := ctx.BuildInstance(instances[0])
514 if ret.Err() != nil {
515 return cue.Value{}, ret.Err()
516 }
517 if err := ret.Validate(); err != nil {
518 return cue.Value{}, err
519 }
520 return ret, nil
521}
522
523func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400524 cfg := struct {
525 Name string `json:"name"`
526 Namespace string `json:"namespace"`
527 Description string `json:"description"`
528 Icon string `json:"icon"`
529 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400530 if err := config.Decode(&cfg); err != nil {
531 return cueApp{}, err
532 }
gio44f621b2024-04-29 09:44:38 +0400533 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400534 if err != nil {
535 return cueApp{}, err
536 }
537 return cueApp{
538 name: cfg.Name,
539 description: cfg.Description,
540 icon: template.HTML(cfg.Icon),
541 namespace: cfg.Namespace,
542 schema: schema,
543 cfg: config,
gio308105e2024-04-19 13:12:13 +0400544 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400545 }, nil
546}
547
gio308105e2024-04-19 13:12:13 +0400548func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
549 config, err := ParseCueAppConfig(data)
550 if err != nil {
551 return cueApp{}, err
552 }
553 return newCueApp(config, data)
554}
555
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400556func (a cueApp) Name() string {
557 return a.name
558}
559
gio44f621b2024-04-29 09:44:38 +0400560func (a cueApp) Slug() string {
561 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
562}
563
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400564func (a cueApp) Description() string {
565 return a.description
566}
567
568func (a cueApp) Icon() template.HTML {
569 return a.icon
570}
571
572func (a cueApp) Schema() Schema {
573 return a.schema
574}
575
gioef01fbb2024-04-12 16:52:59 +0400576func (a cueApp) Namespace() string {
577 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400578}
579
gioe72b54f2024-04-22 10:44:41 +0400580func (a cueApp) render(values map[string]any) (rendered, error) {
581 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400582 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400583 Resources: make(CueAppData),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400584 Ports: make([]PortForward, 0),
gio308105e2024-04-19 13:12:13 +0400585 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400586 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400587 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400588 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400589 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400590 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400591 ctx := a.cfg.Context()
592 d := ctx.CompileBytes(buf.Bytes())
593 res := a.cfg.Unify(d).Eval()
594 if err := res.Err(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400595 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400596 }
597 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400598 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400599 }
gio308105e2024-04-19 13:12:13 +0400600 full, err := json.MarshalIndent(res, "", "\t")
601 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400602 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400603 }
604 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400605 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
606 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400607 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400608 }
609 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400610 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400611 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400612 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400613 output := res.LookupPath(cue.ParsePath("output"))
614 i, err := output.Fields()
615 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400616 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400617 }
618 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400619 if contents, err := cueyaml.Encode(i.Value()); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400620 return rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400621 } else {
622 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
623 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400624 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400625 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400626 helpValue := res.LookupPath(cue.ParsePath("help"))
627 if helpValue.Exists() {
628 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400629 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400630 }
631 }
632 url, err := res.LookupPath(cue.ParsePath("url")).String()
633 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400634 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400635 }
636 ret.Url = url
637 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
638 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400639 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400640 }
641 ret.Icon = icon
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400642 return ret, nil
643}
644
gio3cdee592024-04-17 10:15:56 +0400645type cueEnvApp struct {
646 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400647}
648
gio308105e2024-04-19 13:12:13 +0400649func NewCueEnvApp(data CueAppData) (EnvApp, error) {
650 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400651 if err != nil {
652 return nil, err
653 }
gio3cdee592024-04-17 10:15:56 +0400654 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400655}
656
gio3cdee592024-04-17 10:15:56 +0400657func (a cueEnvApp) Type() AppType {
658 return AppTypeEnv
659}
660
gioe72b54f2024-04-22 10:44:41 +0400661func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error) {
gio3cdee592024-04-17 10:15:56 +0400662 networks := CreateNetworks(env)
663 derived, err := deriveValues(values, a.Schema(), networks)
664 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400665 return EnvAppRendered{}, nil
gio3cdee592024-04-17 10:15:56 +0400666 }
667 ret, err := a.cueApp.render(map[string]any{
668 "global": env,
669 "release": release,
670 "input": derived,
671 })
672 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400673 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400674 }
gioe72b54f2024-04-22 10:44:41 +0400675 return EnvAppRendered{
676 rendered: ret,
677 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400678 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400679 Env: env,
680 Release: release,
681 Values: values,
682 Input: derived,
683 Help: ret.Help,
684 Url: ret.Url,
685 },
686 }, nil
gio3cdee592024-04-17 10:15:56 +0400687}
688
689type cueInfraApp struct {
690 cueApp
691}
692
gio308105e2024-04-19 13:12:13 +0400693func NewCueInfraApp(data CueAppData) (InfraApp, error) {
694 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400695 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400696 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400697 }
gio3cdee592024-04-17 10:15:56 +0400698 return cueInfraApp{app}, nil
699}
700
701func (a cueInfraApp) Type() AppType {
702 return AppTypeInfra
703}
704
gioe72b54f2024-04-22 10:44:41 +0400705func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error) {
706 ret, err := a.cueApp.render(map[string]any{
gio3cdee592024-04-17 10:15:56 +0400707 "global": infra,
708 "release": release,
709 "input": values,
710 })
gioe72b54f2024-04-22 10:44:41 +0400711 if err != nil {
712 return InfraAppRendered{}, err
713 }
714 return InfraAppRendered{
715 rendered: ret,
716 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400717 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400718 Infra: infra,
719 Release: release,
720 Values: values,
721 Input: values,
722 },
723 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400724}
725
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400726func cleanName(s string) string {
727 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400728}
gioe72b54f2024-04-22 10:44:41 +0400729
730func join[T fmt.Stringer](items []T, sep string) string {
731 var tmp []string
732 for _, i := range items {
733 tmp = append(tmp, i.String())
734 }
735 return strings.Join(tmp, ",")
736}