blob: 2a16c1e0d8e04ba30c78a6d9faba32d02320d5a2 [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"
16 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040017 cueyaml "cuelang.org/go/encoding/yaml"
giolekva8aa73e82022-07-09 11:34:39 +040018)
giolekva050609f2021-12-29 15:51:40 +040019
gio0eaf2712024-04-14 13:08:46 +040020//go:embed pcloud_app.cue
21var DodoAppCue []byte
22
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040023// TODO(gio): import
gioe72b54f2024-04-22 10:44:41 +040024const cueEnvAppGlobal = `
gio0eaf2712024-04-14 13:08:46 +040025import (
26 "net"
27)
28
gioe72b54f2024-04-22 10:44:41 +040029#Global: {
30 id: string | *""
31 pcloudEnvName: string | *""
32 domain: string | *""
33 privateDomain: string | *""
34 contactEmail: string | *""
35 adminPublicKey: string | *""
36 publicIP: [...string] | *[]
37 nameserverIP: [...string] | *[]
38 namespacePrefix: string | *""
39 network: #EnvNetwork
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040040}
41
gio0eaf2712024-04-14 13:08:46 +040042#EnvNetwork: {
43 dns: net.IPv4
44 dnsInClusterIP: net.IPv4
45 ingress: net.IPv4
46 headscale: net.IPv4
47 servicesFrom: net.IPv4
48 servicesTo: net.IPv4
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040049}
50
gioe72b54f2024-04-22 10:44:41 +040051// TODO(gio): remove
52ingressPrivate: "\(global.id)-ingress-private"
53ingressPublic: "\(global.pcloudEnvName)-ingress-public"
54issuerPrivate: "\(global.id)-private"
55issuerPublic: "\(global.id)-public"
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040056
gio1de49582024-04-21 08:33:57 +040057#Ingress: {
58 auth: #Auth
59 network: #Network
60 subdomain: string
61 service: close({
62 name: string
63 port: close({ name: string }) | close({ number: int & > 0 })
64 })
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040065
gio1de49582024-04-21 08:33:57 +040066 _domain: "\(subdomain).\(network.domain)"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040067 _authProxyHTTPPortName: "http"
68
69 out: {
70 images: {
71 authProxy: #Image & {
72 repository: "giolekva"
73 name: "auth-proxy"
74 tag: "latest"
75 pullPolicy: "Always"
76 }
77 }
78 charts: {
79 ingress: #Chart & {
80 chart: "charts/ingress"
81 sourceRef: {
82 kind: "GitRepository"
83 name: "pcloud"
84 namespace: global.id
85 }
86 }
87 authProxy: #Chart & {
88 chart: "charts/auth-proxy"
89 sourceRef: {
90 kind: "GitRepository"
91 name: "pcloud"
92 namespace: global.id
93 }
94 }
95 }
96 helm: {
gio1de49582024-04-21 08:33:57 +040097 if auth.enabled {
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040098 "auth-proxy": {
99 chart: charts.authProxy
100 values: {
101 image: {
102 repository: images.authProxy.fullName
103 tag: images.authProxy.tag
104 pullPolicy: images.authProxy.pullPolicy
105 }
gio1de49582024-04-21 08:33:57 +0400106 upstream: "\(service.name).\(release.namespace).svc.cluster.local"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400107 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
108 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400109 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
gio1de49582024-04-21 08:33:57 +0400110 groups: auth.groups
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400111 portName: _authProxyHTTPPortName
112 }
113 }
114 }
115 ingress: {
116 chart: charts.ingress
gio1de49582024-04-21 08:33:57 +0400117 _service: service
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400118 values: {
119 domain: _domain
gio1de49582024-04-21 08:33:57 +0400120 ingressClassName: network.ingressClass
121 certificateIssuer: network.certificateIssuer
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400122 service: {
gio1de49582024-04-21 08:33:57 +0400123 if auth.enabled {
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400124 name: "auth-proxy"
125 port: name: _authProxyHTTPPortName
126 }
gio1de49582024-04-21 08:33:57 +0400127 if !auth.enabled {
128 name: _service.name
129 if _service.port.name != _|_ {
130 port: name: _service.port.name
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400131 }
gio1de49582024-04-21 08:33:57 +0400132 if _service.port.number != _|_ {
133 port: number: _service.port.number
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400134 }
135 }
136 }
137 }
138 }
139 }
140 }
141}
142
gio1de49582024-04-21 08:33:57 +0400143ingress: {}
144
145_ingressValidate: {
146 for key, value in ingress {
147 "\(key)": #Ingress & value
148 }
149}
gioe72b54f2024-04-22 10:44:41 +0400150`
151
152const cueInfraAppGlobal = `
153#Global: {
154 pcloudEnvName: string | *""
155 publicIP: [...string] | *[]
156 namespacePrefix: string | *""
157 infraAdminPublicKey: string | *""
158}
159
160// TODO(gio): remove
161ingressPublic: "\(global.pcloudEnvName)-ingress-public"
162
163ingress: {}
164_ingressValidate: {}
165`
166
167const cueBaseConfig = `
gioe72b54f2024-04-22 10:44:41 +0400168name: string | *""
169description: string | *""
170readme: string | *""
171icon: string | *""
172namespace: string | *""
173
174help: [...#HelpDocument] | *[]
175
176#HelpDocument: {
177 title: string
178 contents: string
179 children: [...#HelpDocument] | *[]
180}
181
182url: string | *""
183
184#AppType: "infra" | "env"
185appType: #AppType | *"env"
186
gio0eaf2712024-04-14 13:08:46 +0400187#Release: {
188 appInstanceId: string
189 namespace: string
190 repoAddr: string
191 appDir: string
gioe72b54f2024-04-22 10:44:41 +0400192}
193
194#Network: {
195 name: string
196 ingressClass: string
197 certificateIssuer: string | *""
198 domain: string
199 allocatePortAddr: string
200}
201
gio0eaf2712024-04-14 13:08:46 +0400202#Auth: {
203 enabled: bool | *false // TODO(gio): enabled by default?
204 groups: string | *"" // TODO(gio): []string
205}
206
gioe72b54f2024-04-22 10:44:41 +0400207#Image: {
208 registry: string | *"docker.io"
209 repository: string
210 name: string
211 tag: string
212 pullPolicy: string | *"IfNotPresent"
213 imageName: "\(repository)/\(name)"
214 fullName: "\(registry)/\(imageName)"
215 fullNameWithTag: "\(fullName):\(tag)"
216}
217
218#Chart: {
219 chart: string
220 sourceRef: #SourceRef
221}
222
223#SourceRef: {
224 kind: "GitRepository" | "HelmRepository"
225 name: string
226 namespace: string // TODO(gio): default global.id
227}
228
gioe72b54f2024-04-22 10:44:41 +0400229#PortForward: {
230 allocator: string
231 protocol: "TCP" | "UDP" | *"TCP"
232 sourcePort: int
233 targetService: string
234 targetPort: int
235}
236
237portForward: [...#PortForward] | *[]
238
239global: #Global
240release: #Release
gio1de49582024-04-21 08:33:57 +0400241
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400242images: {
243 for key, value in images {
244 "\(key)": #Image & value
245 }
gio1de49582024-04-21 08:33:57 +0400246 for _, value in _ingressValidate {
247 for name, image in value.out.images {
248 "\(name)": #Image & image
249 }
250 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400251}
252
253charts: {
254 for key, value in charts {
255 "\(key)": #Chart & value
256 }
gio1de49582024-04-21 08:33:57 +0400257 for _, value in _ingressValidate {
258 for name, chart in value.out.charts {
259 "\(name)": #Chart & chart
260 }
261 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400262}
263
264#ResourceReference: {
265 name: string
266 namespace: string
267}
268
269#Helm: {
270 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400271 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400272 ...
273}
274
gio1de49582024-04-21 08:33:57 +0400275_helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400276 for key, value in helm {
277 "\(key)": #Helm & value & {
278 name: key
279 }
280 }
gio1de49582024-04-21 08:33:57 +0400281 for key, value in _ingressValidate {
282 for ing, ingValue in value.out.helm {
283 // TODO(gio): support multiple ingresses
284 // "\(key)-\(ing)": #Helm & ingValue & {
285 "\(ing)": #Helm & ingValue & {
286 // name: "\(key)-\(ing)"
287 name: ing
288 }
289 }
290 }
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400291}
292
gio0eaf2712024-04-14 13:08:46 +0400293resources: {}
294
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400295#HelmRelease: {
296 _name: string
297 _chart: #Chart
298 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400299 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400300
301 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
302 kind: "HelmRelease"
303 metadata: {
304 name: _name
305 namespace: release.namespace
306 }
307 spec: {
308 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400309 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400310 chart: {
311 spec: _chart
312 }
313 values: _values
314 }
315}
316
317output: {
gio1de49582024-04-21 08:33:57 +0400318 for name, r in _helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400319 "\(name)": #HelmRelease & {
320 _name: name
321 _chart: r.chart
322 _values: r.values
323 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400324 }
325 }
326}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400327
328#SSHKey: {
329 public: string
330 private: string
331}
gio09a3e5b2024-04-26 14:11:06 +0400332
333#HelpDocument: {
334 title: string
335 contents: string
336 children: [...#HelpDocument]
337}
338
339help: [...#HelpDocument] | *[]
340
341url: string | *""
gio0eaf2712024-04-14 13:08:46 +0400342
343networks: {}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400344`
345
gioe72b54f2024-04-22 10:44:41 +0400346type rendered struct {
gio3cdee592024-04-17 10:15:56 +0400347 Name string
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400348 Readme string
gio308105e2024-04-19 13:12:13 +0400349 Resources CueAppData
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400350 Ports []PortForward
gio308105e2024-04-19 13:12:13 +0400351 Data CueAppData
gio09a3e5b2024-04-26 14:11:06 +0400352 URL string
Davit Tabidze56f86a42024-04-09 19:15:25 +0400353 Help []HelpDocument
Davit Tabidze56f86a42024-04-09 19:15:25 +0400354 Icon string
355}
356
357type HelpDocument struct {
358 Title string
359 Contents string
360 Children []HelpDocument
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400361}
362
gioe72b54f2024-04-22 10:44:41 +0400363type EnvAppRendered struct {
364 rendered
365 Config AppInstanceConfig
366}
367
368type InfraAppRendered struct {
369 rendered
370 Config InfraAppInstanceConfig
371}
372
gio3cdee592024-04-17 10:15:56 +0400373type PortForward struct {
374 Allocator string `json:"allocator"`
375 Protocol string `json:"protocol"`
376 SourcePort int `json:"sourcePort"`
377 TargetService string `json:"targetService"`
378 TargetPort int `json:"targetPort"`
379}
380
381type AppType int
382
383const (
384 AppTypeInfra AppType = iota
385 AppTypeEnv
386)
387
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400388type App interface {
389 Name() string
gio44f621b2024-04-29 09:44:38 +0400390 Type() AppType
391 Slug() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400392 Description() string
393 Icon() template.HTML
394 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400395 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400396}
397
398type InfraConfig struct {
gioe72b54f2024-04-22 10:44:41 +0400399 Name string `json:"pcloudEnvName,omitempty"` // #TODO(gio): change to name
400 PublicIP []net.IP `json:"publicIP,omitempty"`
401 InfraNamespacePrefix string `json:"namespacePrefix,omitempty"`
402 InfraAdminPublicKey []byte `json:"infraAdminPublicKey,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400403}
404
405type InfraApp interface {
406 App
gioe72b54f2024-04-22 10:44:41 +0400407 Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error)
408}
409
410type EnvNetwork struct {
411 DNS net.IP `json:"dns,omitempty"`
412 DNSInClusterIP net.IP `json:"dnsInClusterIP,omitempty"`
413 Ingress net.IP `json:"ingress,omitempty"`
414 Headscale net.IP `json:"headscale,omitempty"`
415 ServicesFrom net.IP `json:"servicesFrom,omitempty"`
416 ServicesTo net.IP `json:"servicesTo,omitempty"`
417}
418
419func NewEnvNetwork(subnet net.IP) (EnvNetwork, error) {
420 addr, err := netip.ParseAddr(subnet.String())
421 if err != nil {
422 return EnvNetwork{}, err
423 }
424 if !addr.Is4() {
425 return EnvNetwork{}, fmt.Errorf("Expected IPv4, got %s instead", addr)
426 }
427 dns := addr.Next()
428 ingress := dns.Next()
429 headscale := ingress.Next()
430 b := addr.AsSlice()
431 if b[3] != 0 {
432 return EnvNetwork{}, fmt.Errorf("Expected last byte to be zero, got %d instead", b[3])
433 }
434 b[3] = 10
435 servicesFrom, ok := netip.AddrFromSlice(b)
436 if !ok {
437 return EnvNetwork{}, fmt.Errorf("Must not reach")
438 }
439 b[3] = 254
440 servicesTo, ok := netip.AddrFromSlice(b)
441 if !ok {
442 return EnvNetwork{}, fmt.Errorf("Must not reach")
443 }
444 b[3] = b[2]
445 b[2] = b[1]
446 b[0] = 10
447 b[1] = 44
448 dnsInClusterIP, ok := netip.AddrFromSlice(b)
449 if !ok {
450 return EnvNetwork{}, fmt.Errorf("Must not reach")
451 }
452 return EnvNetwork{
453 DNS: net.ParseIP(dns.String()),
454 DNSInClusterIP: net.ParseIP(dnsInClusterIP.String()),
455 Ingress: net.ParseIP(ingress.String()),
456 Headscale: net.ParseIP(headscale.String()),
457 ServicesFrom: net.ParseIP(servicesFrom.String()),
458 ServicesTo: net.ParseIP(servicesTo.String()),
459 }, nil
gio3cdee592024-04-17 10:15:56 +0400460}
461
462// TODO(gio): rename to EnvConfig
gioe72b54f2024-04-22 10:44:41 +0400463type EnvConfig struct {
464 Id string `json:"id,omitempty"`
465 InfraName string `json:"pcloudEnvName,omitempty"`
466 Domain string `json:"domain,omitempty"`
467 PrivateDomain string `json:"privateDomain,omitempty"`
468 ContactEmail string `json:"contactEmail,omitempty"`
469 AdminPublicKey string `json:"adminPublicKey,omitempty"`
470 PublicIP []net.IP `json:"publicIP,omitempty"`
471 NameserverIP []net.IP `json:"nameserverIP,omitempty"`
472 NamespacePrefix string `json:"namespacePrefix,omitempty"`
473 Network EnvNetwork `json:"network,omitempty"`
gio3cdee592024-04-17 10:15:56 +0400474}
475
476type EnvApp interface {
477 App
gioe72b54f2024-04-22 10:44:41 +0400478 Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400479}
480
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400481type cueApp struct {
482 name string
483 description string
484 icon template.HTML
485 namespace string
486 schema Schema
gio308105e2024-04-19 13:12:13 +0400487 cfg cue.Value
488 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400489}
490
gio308105e2024-04-19 13:12:13 +0400491type CueAppData map[string][]byte
492
493func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
494 ctx := cuecontext.New()
495 buildCtx := build.NewContext()
496 cfg := &load.Config{
497 Context: buildCtx,
498 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400499 }
gio308105e2024-04-19 13:12:13 +0400500 names := make([]string, 0)
501 for n, b := range data {
502 a := fmt.Sprintf("/%s", n)
503 names = append(names, a)
504 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
505 }
506 instances := load.Instances(names, cfg)
507 for _, inst := range instances {
508 if inst.Err != nil {
509 return cue.Value{}, inst.Err
510 }
511 }
512 if len(instances) != 1 {
513 return cue.Value{}, fmt.Errorf("invalid")
514 }
515 ret := ctx.BuildInstance(instances[0])
516 if ret.Err() != nil {
517 return cue.Value{}, ret.Err()
518 }
519 if err := ret.Validate(); err != nil {
520 return cue.Value{}, err
521 }
522 return ret, nil
523}
524
525func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400526 cfg := struct {
527 Name string `json:"name"`
528 Namespace string `json:"namespace"`
529 Description string `json:"description"`
530 Icon string `json:"icon"`
531 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400532 if err := config.Decode(&cfg); err != nil {
533 return cueApp{}, err
534 }
gio44f621b2024-04-29 09:44:38 +0400535 schema, err := NewCueSchema("input", config.LookupPath(cue.ParsePath("input")))
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400536 if err != nil {
537 return cueApp{}, err
538 }
539 return cueApp{
540 name: cfg.Name,
541 description: cfg.Description,
542 icon: template.HTML(cfg.Icon),
543 namespace: cfg.Namespace,
544 schema: schema,
545 cfg: config,
gio308105e2024-04-19 13:12:13 +0400546 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400547 }, nil
548}
549
gio308105e2024-04-19 13:12:13 +0400550func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
551 config, err := ParseCueAppConfig(data)
552 if err != nil {
553 return cueApp{}, err
554 }
555 return newCueApp(config, data)
556}
557
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400558func (a cueApp) Name() string {
559 return a.name
560}
561
gio44f621b2024-04-29 09:44:38 +0400562func (a cueApp) Slug() string {
563 return strings.ReplaceAll(strings.ToLower(a.name), " ", "-")
564}
565
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400566func (a cueApp) Description() string {
567 return a.description
568}
569
570func (a cueApp) Icon() template.HTML {
571 return a.icon
572}
573
574func (a cueApp) Schema() Schema {
575 return a.schema
576}
577
gioef01fbb2024-04-12 16:52:59 +0400578func (a cueApp) Namespace() string {
579 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400580}
581
gioe72b54f2024-04-22 10:44:41 +0400582func (a cueApp) render(values map[string]any) (rendered, error) {
583 ret := rendered{
gio44f621b2024-04-29 09:44:38 +0400584 Name: a.Slug(),
gio308105e2024-04-19 13:12:13 +0400585 Resources: make(CueAppData),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400586 Ports: make([]PortForward, 0),
gio308105e2024-04-19 13:12:13 +0400587 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400588 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400589 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400590 if err := json.NewEncoder(&buf).Encode(values); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400591 return rendered{}, err
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400592 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400593 ctx := a.cfg.Context()
594 d := ctx.CompileBytes(buf.Bytes())
595 res := a.cfg.Unify(d).Eval()
596 if err := res.Err(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400597 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400598 }
599 if err := res.Validate(); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400600 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400601 }
gio308105e2024-04-19 13:12:13 +0400602 full, err := json.MarshalIndent(res, "", "\t")
603 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400604 return rendered{}, err
gio308105e2024-04-19 13:12:13 +0400605 }
606 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400607 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
608 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400609 return rendered{}, err
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400610 }
611 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400612 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400613 return rendered{}, err
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400614 }
gio0eaf2712024-04-14 13:08:46 +0400615 {
616 output := res.LookupPath(cue.ParsePath("output"))
617 i, err := output.Fields()
618 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400619 return rendered{}, err
gio0eaf2712024-04-14 13:08:46 +0400620 }
621 for i.Next() {
622 if contents, err := cueyaml.Encode(i.Value()); err != nil {
623 return rendered{}, err
624 } else {
625 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
626 ret.Resources[name] = contents
627 }
628 }
629 }
630 {
631 resources := res.LookupPath(cue.ParsePath("resources"))
632 i, err := resources.Fields()
633 if err != nil {
634 return rendered{}, err
635 }
636 for i.Next() {
637 if contents, err := cueyaml.Encode(i.Value()); err != nil {
638 return rendered{}, err
639 } else {
640 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
641 ret.Resources[name] = contents
642 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400643 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400644 }
Davit Tabidze56f86a42024-04-09 19:15:25 +0400645 helpValue := res.LookupPath(cue.ParsePath("help"))
646 if helpValue.Exists() {
647 if err := helpValue.Decode(&ret.Help); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400648 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400649 }
650 }
651 url, err := res.LookupPath(cue.ParsePath("url")).String()
652 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400653 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400654 }
gio09a3e5b2024-04-26 14:11:06 +0400655 ret.URL = url
Davit Tabidze56f86a42024-04-09 19:15:25 +0400656 icon, err := res.LookupPath(cue.ParsePath("icon")).String()
657 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400658 return rendered{}, err
Davit Tabidze56f86a42024-04-09 19:15:25 +0400659 }
660 ret.Icon = icon
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400661 return ret, nil
662}
663
gio3cdee592024-04-17 10:15:56 +0400664type cueEnvApp struct {
665 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400666}
667
gio308105e2024-04-19 13:12:13 +0400668func NewCueEnvApp(data CueAppData) (EnvApp, error) {
669 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400670 if err != nil {
671 return nil, err
672 }
gio3cdee592024-04-17 10:15:56 +0400673 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400674}
675
gio0eaf2712024-04-14 13:08:46 +0400676func NewDodoApp(appCfg []byte) (EnvApp, error) {
677 return NewCueEnvApp(CueAppData{
678 "app.cue": appCfg,
679 "base.cue": []byte(cueBaseConfig),
680 "pcloud_app.cue": DodoAppCue,
681 "env_app.cue": []byte(cueEnvAppGlobal),
682 })
683}
684
gio3cdee592024-04-17 10:15:56 +0400685func (a cueEnvApp) Type() AppType {
686 return AppTypeEnv
687}
688
gioe72b54f2024-04-22 10:44:41 +0400689func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any) (EnvAppRendered, error) {
gio3cdee592024-04-17 10:15:56 +0400690 networks := CreateNetworks(env)
691 derived, err := deriveValues(values, a.Schema(), networks)
692 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400693 return EnvAppRendered{}, nil
gio3cdee592024-04-17 10:15:56 +0400694 }
695 ret, err := a.cueApp.render(map[string]any{
gio0eaf2712024-04-14 13:08:46 +0400696 "global": env,
697 "release": release,
698 "input": derived,
699 "networks": networkMap(networks),
gio3cdee592024-04-17 10:15:56 +0400700 })
701 if err != nil {
gioe72b54f2024-04-22 10:44:41 +0400702 return EnvAppRendered{}, err
gio3cdee592024-04-17 10:15:56 +0400703 }
gioe72b54f2024-04-22 10:44:41 +0400704 return EnvAppRendered{
705 rendered: ret,
706 Config: AppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400707 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400708 Env: env,
709 Release: release,
710 Values: values,
711 Input: derived,
gio09a3e5b2024-04-26 14:11:06 +0400712 URL: ret.URL,
gioe72b54f2024-04-22 10:44:41 +0400713 Help: ret.Help,
gio09a3e5b2024-04-26 14:11:06 +0400714 Icon: ret.Icon,
gioe72b54f2024-04-22 10:44:41 +0400715 },
716 }, nil
gio3cdee592024-04-17 10:15:56 +0400717}
718
719type cueInfraApp struct {
720 cueApp
721}
722
gio308105e2024-04-19 13:12:13 +0400723func NewCueInfraApp(data CueAppData) (InfraApp, error) {
724 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400725 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400726 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400727 }
gio3cdee592024-04-17 10:15:56 +0400728 return cueInfraApp{app}, nil
729}
730
731func (a cueInfraApp) Type() AppType {
732 return AppTypeInfra
733}
734
gioe72b54f2024-04-22 10:44:41 +0400735func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (InfraAppRendered, error) {
736 ret, err := a.cueApp.render(map[string]any{
gio3cdee592024-04-17 10:15:56 +0400737 "global": infra,
738 "release": release,
739 "input": values,
740 })
gioe72b54f2024-04-22 10:44:41 +0400741 if err != nil {
742 return InfraAppRendered{}, err
743 }
744 return InfraAppRendered{
745 rendered: ret,
746 Config: InfraAppInstanceConfig{
gio44f621b2024-04-29 09:44:38 +0400747 AppId: a.Slug(),
gioe72b54f2024-04-22 10:44:41 +0400748 Infra: infra,
749 Release: release,
750 Values: values,
751 Input: values,
gio09a3e5b2024-04-26 14:11:06 +0400752 URL: ret.URL,
753 Help: ret.Help,
gioe72b54f2024-04-22 10:44:41 +0400754 },
755 }, nil
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400756}
757
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400758func cleanName(s string) string {
759 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400760}
gioe72b54f2024-04-22 10:44:41 +0400761
762func join[T fmt.Stringer](items []T, sep string) string {
763 var tmp []string
764 for _, i := range items {
765 tmp = append(tmp, i.String())
766 }
767 return strings.Join(tmp, ",")
768}
gio0eaf2712024-04-14 13:08:46 +0400769
770func networkMap(networks []Network) map[string]Network {
771 ret := make(map[string]Network)
772 for _, n := range networks {
773 ret[strings.ToLower(n.Name)] = n
774 }
775 return ret
776}