blob: 554593ea76e42ed4b75e89e7e751dcfb1c3d05f3 [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"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04009 "strings"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040010
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040011 "cuelang.org/go/cue"
gio308105e2024-04-19 13:12:13 +040012 "cuelang.org/go/cue/build"
13 "cuelang.org/go/cue/cuecontext"
14 "cuelang.org/go/cue/load"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040015 cueyaml "cuelang.org/go/encoding/yaml"
giolekva8aa73e82022-07-09 11:34:39 +040016)
giolekva050609f2021-12-29 15:51:40 +040017
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040018// TODO(gio): import
19const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040020name: string | *""
21description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040022readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040023icon: string | *""
24namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040025
gio308105e2024-04-19 13:12:13 +040026#AppType: "infra" | "env"
27appType: #AppType | *"env"
28
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040029#Auth: {
30 enabled: bool | *false // TODO(gio): enabled by default?
31 groups: string | *"" // TODO(gio): []string
32}
33
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040034#Network: {
35 name: string
36 ingressClass: string
37 certificateIssuer: string | *""
38 domain: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040039 allocatePortAddr: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040040}
41
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040042networks: {
43 public: #Network & {
44 name: "Public"
45 ingressClass: "\(global.pcloudEnvName)-ingress-public"
46 certificateIssuer: "\(global.id)-public"
47 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040048 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040049 }
50 private: #Network & {
51 name: "Private"
52 ingressClass: "\(global.id)-ingress-private"
53 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040054 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040055 }
56}
57
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040058#Image: {
59 registry: string | *"docker.io"
60 repository: string
61 name: string
62 tag: string
63 pullPolicy: string | *"IfNotPresent"
64 imageName: "\(repository)/\(name)"
65 fullName: "\(registry)/\(imageName)"
66 fullNameWithTag: "\(fullName):\(tag)"
67}
68
69#Chart: {
70 chart: string
71 sourceRef: #SourceRef
72}
73
74#SourceRef: {
75 kind: "GitRepository" | "HelmRepository"
76 name: string
77 namespace: string // TODO(gio): default global.id
78}
79
80#Global: {
81 id: string | *""
82 pcloudEnvName: string | *""
83 domain: string | *""
84 privateDomain: string | *""
85 namespacePrefix: string | *""
86 ...
87}
88
89#Release: {
gio3cdee592024-04-17 10:15:56 +040090 appInstanceId: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040091 namespace: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040092 repoAddr: string
93 appDir: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040094}
95
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040096#PortForward: {
97 allocator: string
98 protocol: "TCP" | "UDP" | *"TCP"
99 sourcePort: int
100 targetService: string
101 targetPort: int
102}
103
104portForward: [...#PortForward] | *[]
105
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400106global: #Global
107release: #Release
108
109_ingressPrivate: "\(global.id)-ingress-private"
110_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
111_issuerPrivate: "\(global.id)-private"
112_issuerPublic: "\(global.id)-public"
113
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400114_IngressWithAuthProxy: {
115 inp: {
116 auth: #Auth
117 network: #Network
118 subdomain: string
119 serviceName: string
120 port: { name: string } | { number: int & > 0 }
121 }
122
123 _domain: "\(inp.subdomain).\(inp.network.domain)"
124 _authProxyHTTPPortName: "http"
125
126 out: {
127 images: {
128 authProxy: #Image & {
129 repository: "giolekva"
130 name: "auth-proxy"
131 tag: "latest"
132 pullPolicy: "Always"
133 }
134 }
135 charts: {
136 ingress: #Chart & {
137 chart: "charts/ingress"
138 sourceRef: {
139 kind: "GitRepository"
140 name: "pcloud"
141 namespace: global.id
142 }
143 }
144 authProxy: #Chart & {
145 chart: "charts/auth-proxy"
146 sourceRef: {
147 kind: "GitRepository"
148 name: "pcloud"
149 namespace: global.id
150 }
151 }
152 }
153 helm: {
154 if inp.auth.enabled {
155 "auth-proxy": {
156 chart: charts.authProxy
157 values: {
158 image: {
159 repository: images.authProxy.fullName
160 tag: images.authProxy.tag
161 pullPolicy: images.authProxy.pullPolicy
162 }
163 upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
164 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
165 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400166 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400167 groups: inp.auth.groups
168 portName: _authProxyHTTPPortName
169 }
170 }
171 }
172 ingress: {
173 chart: charts.ingress
174 values: {
175 domain: _domain
176 ingressClassName: inp.network.ingressClass
177 certificateIssuer: inp.network.certificateIssuer
178 service: {
179 if inp.auth.enabled {
180 name: "auth-proxy"
181 port: name: _authProxyHTTPPortName
182 }
183 if !inp.auth.enabled {
184 name: inp.serviceName
185 if inp.port.name != _|_ {
186 port: name: inp.port.name
187 }
188 if inp.port.number != _|_ {
189 port: number: inp.port.number
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197}
198
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400199images: {
200 for key, value in images {
201 "\(key)": #Image & value
202 }
203}
204
205charts: {
206 for key, value in charts {
207 "\(key)": #Chart & value
208 }
209}
210
211#ResourceReference: {
212 name: string
213 namespace: string
214}
215
216#Helm: {
217 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400218 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400219 ...
220}
221
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400222helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400223 for key, value in helm {
224 "\(key)": #Helm & value & {
225 name: key
226 }
227 }
228}
229
230#HelmRelease: {
231 _name: string
232 _chart: #Chart
233 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400234 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400235
236 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
237 kind: "HelmRelease"
238 metadata: {
239 name: _name
240 namespace: release.namespace
241 }
242 spec: {
243 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400244 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400245 chart: {
246 spec: _chart
247 }
248 values: _values
249 }
250}
251
252output: {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400253 for name, r in helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400254 "\(name)": #HelmRelease & {
255 _name: name
256 _chart: r.chart
257 _values: r.values
258 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400259 }
260 }
261}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400262
263#SSHKey: {
264 public: string
265 private: string
266}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400267`
268
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400269type Rendered struct {
gio3cdee592024-04-17 10:15:56 +0400270 Name string
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400271 Readme string
gio308105e2024-04-19 13:12:13 +0400272 Resources CueAppData
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400273 Ports []PortForward
gio3cdee592024-04-17 10:15:56 +0400274 Config AppInstanceConfig
gio308105e2024-04-19 13:12:13 +0400275 Data CueAppData
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400276}
277
gio3cdee592024-04-17 10:15:56 +0400278type PortForward struct {
279 Allocator string `json:"allocator"`
280 Protocol string `json:"protocol"`
281 SourcePort int `json:"sourcePort"`
282 TargetService string `json:"targetService"`
283 TargetPort int `json:"targetPort"`
284}
285
286type AppType int
287
288const (
289 AppTypeInfra AppType = iota
290 AppTypeEnv
291)
292
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400293type App interface {
gio3cdee592024-04-17 10:15:56 +0400294 Type() AppType
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400295 Name() string
296 Description() string
297 Icon() template.HTML
298 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400299 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400300}
301
302type InfraConfig struct {
303 Name string `json:"pcloudEnvName"` // #TODO(gio): change to name
304 PublicIP []net.IP `json:"publicIP"`
305 InfraNamespacePrefix string `json:"namespacePrefix"`
306 InfraAdminPublicKey []byte `json:"infraAdminPublicKey"`
307}
308
309type InfraApp interface {
310 App
311 Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error)
312}
313
314// TODO(gio): rename to EnvConfig
315type AppEnvConfig struct {
316 Id string `json:"id"`
317 InfraName string `json:"pcloudEnvName"`
318 Domain string `json:"domain"`
319 PrivateDomain string `json:"privateDomain"`
320 ContactEmail string `json:"contactEmail"`
321 PublicIP []net.IP `json:"publicIP"`
322 NamespacePrefix string `json:"namespacePrefix"`
323}
324
325type EnvApp interface {
326 App
327 Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400328}
329
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400330type cueApp struct {
331 name string
332 description string
333 icon template.HTML
334 namespace string
335 schema Schema
gio308105e2024-04-19 13:12:13 +0400336 cfg cue.Value
337 data CueAppData
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400338}
339
gio308105e2024-04-19 13:12:13 +0400340type CueAppData map[string][]byte
341
342func ParseCueAppConfig(data CueAppData) (cue.Value, error) {
343 ctx := cuecontext.New()
344 buildCtx := build.NewContext()
345 cfg := &load.Config{
346 Context: buildCtx,
347 Overlay: map[string]load.Source{},
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400348 }
gio308105e2024-04-19 13:12:13 +0400349 names := make([]string, 0)
350 for n, b := range data {
351 a := fmt.Sprintf("/%s", n)
352 names = append(names, a)
353 cfg.Overlay[a] = load.FromString("package main\n\n" + string(b))
354 }
355 instances := load.Instances(names, cfg)
356 for _, inst := range instances {
357 if inst.Err != nil {
358 return cue.Value{}, inst.Err
359 }
360 }
361 if len(instances) != 1 {
362 return cue.Value{}, fmt.Errorf("invalid")
363 }
364 ret := ctx.BuildInstance(instances[0])
365 if ret.Err() != nil {
366 return cue.Value{}, ret.Err()
367 }
368 if err := ret.Validate(); err != nil {
369 return cue.Value{}, err
370 }
371 return ret, nil
372}
373
374func newCueApp(config cue.Value, data CueAppData) (cueApp, error) {
gio3cdee592024-04-17 10:15:56 +0400375 cfg := struct {
376 Name string `json:"name"`
377 Namespace string `json:"namespace"`
378 Description string `json:"description"`
379 Icon string `json:"icon"`
380 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400381 if err := config.Decode(&cfg); err != nil {
382 return cueApp{}, err
383 }
384 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
385 if err != nil {
386 return cueApp{}, err
387 }
388 return cueApp{
389 name: cfg.Name,
390 description: cfg.Description,
391 icon: template.HTML(cfg.Icon),
392 namespace: cfg.Namespace,
393 schema: schema,
394 cfg: config,
gio308105e2024-04-19 13:12:13 +0400395 data: data,
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400396 }, nil
397}
398
gio308105e2024-04-19 13:12:13 +0400399func ParseAndCreateNewCueApp(data CueAppData) (cueApp, error) {
400 config, err := ParseCueAppConfig(data)
401 if err != nil {
402 return cueApp{}, err
403 }
404 return newCueApp(config, data)
405}
406
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400407func (a cueApp) Name() string {
408 return a.name
409}
410
411func (a cueApp) Description() string {
412 return a.description
413}
414
415func (a cueApp) Icon() template.HTML {
416 return a.icon
417}
418
419func (a cueApp) Schema() Schema {
420 return a.schema
421}
422
gioef01fbb2024-04-12 16:52:59 +0400423func (a cueApp) Namespace() string {
424 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400425}
426
gio3cdee592024-04-17 10:15:56 +0400427func (a cueApp) render(values map[string]any) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400428 ret := Rendered{
gio3cdee592024-04-17 10:15:56 +0400429 Name: a.Name(),
gio308105e2024-04-19 13:12:13 +0400430 Resources: make(CueAppData),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400431 Ports: make([]PortForward, 0),
gio308105e2024-04-19 13:12:13 +0400432 Data: a.data,
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400433 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400434 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400435 if err := json.NewEncoder(&buf).Encode(values); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400436 return Rendered{}, err
437 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400438 ctx := a.cfg.Context()
439 d := ctx.CompileBytes(buf.Bytes())
440 res := a.cfg.Unify(d).Eval()
441 if err := res.Err(); err != nil {
442 return Rendered{}, err
443 }
444 if err := res.Validate(); err != nil {
445 return Rendered{}, err
446 }
gio308105e2024-04-19 13:12:13 +0400447 full, err := json.MarshalIndent(res, "", "\t")
448 if err != nil {
449 return Rendered{}, err
450 }
451 ret.Data["rendered.json"] = full
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400452 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
453 if err != nil {
454 return Rendered{}, err
455 }
456 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400457 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
458 return Rendered{}, err
459 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400460 output := res.LookupPath(cue.ParsePath("output"))
461 i, err := output.Fields()
462 if err != nil {
463 return Rendered{}, err
464 }
465 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400466 if contents, err := cueyaml.Encode(i.Value()); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400467 return Rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400468 } else {
469 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
470 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400471 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400472 }
473 return ret, nil
474}
475
gio3cdee592024-04-17 10:15:56 +0400476type cueEnvApp struct {
477 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400478}
479
gio308105e2024-04-19 13:12:13 +0400480func NewCueEnvApp(data CueAppData) (EnvApp, error) {
481 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400482 if err != nil {
483 return nil, err
484 }
gio3cdee592024-04-17 10:15:56 +0400485 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400486}
487
gio3cdee592024-04-17 10:15:56 +0400488func (a cueEnvApp) Type() AppType {
489 return AppTypeEnv
490}
491
492func (a cueEnvApp) Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error) {
493 networks := CreateNetworks(env)
494 derived, err := deriveValues(values, a.Schema(), networks)
495 if err != nil {
496 return Rendered{}, nil
497 }
498 ret, err := a.cueApp.render(map[string]any{
499 "global": env,
500 "release": release,
501 "input": derived,
502 })
503 if err != nil {
504 return Rendered{}, err
505 }
506 ret.Config = AppInstanceConfig{
507 AppId: a.Name(),
508 Env: env,
509 Release: release,
510 Values: values,
511 Input: derived,
512 }
513 return ret, nil
514}
515
516type cueInfraApp struct {
517 cueApp
518}
519
gio308105e2024-04-19 13:12:13 +0400520func NewCueInfraApp(data CueAppData) (InfraApp, error) {
521 app, err := ParseAndCreateNewCueApp(data)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400522 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400523 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400524 }
gio3cdee592024-04-17 10:15:56 +0400525 return cueInfraApp{app}, nil
526}
527
528func (a cueInfraApp) Type() AppType {
529 return AppTypeInfra
530}
531
532func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error) {
533 return a.cueApp.render(map[string]any{
534 "global": infra,
535 "release": release,
536 "input": values,
537 })
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400538}
539
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400540func cleanName(s string) string {
541 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400542}