blob: 18f9e751b4d255706c922a0311e27b94be257393 [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"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040012 cueyaml "cuelang.org/go/encoding/yaml"
giolekva8aa73e82022-07-09 11:34:39 +040013)
giolekva050609f2021-12-29 15:51:40 +040014
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040015// TODO(gio): import
16const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040017name: string | *""
18description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040019readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040020icon: string | *""
21namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040022
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040023#Auth: {
24 enabled: bool | *false // TODO(gio): enabled by default?
25 groups: string | *"" // TODO(gio): []string
26}
27
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040028#Network: {
29 name: string
30 ingressClass: string
31 certificateIssuer: string | *""
32 domain: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040033 allocatePortAddr: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040034}
35
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040036networks: {
37 public: #Network & {
38 name: "Public"
39 ingressClass: "\(global.pcloudEnvName)-ingress-public"
40 certificateIssuer: "\(global.id)-public"
41 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040042 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040043 }
44 private: #Network & {
45 name: "Private"
46 ingressClass: "\(global.id)-ingress-private"
47 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040048 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040049 }
50}
51
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040052#Image: {
53 registry: string | *"docker.io"
54 repository: string
55 name: string
56 tag: string
57 pullPolicy: string | *"IfNotPresent"
58 imageName: "\(repository)/\(name)"
59 fullName: "\(registry)/\(imageName)"
60 fullNameWithTag: "\(fullName):\(tag)"
61}
62
63#Chart: {
64 chart: string
65 sourceRef: #SourceRef
66}
67
68#SourceRef: {
69 kind: "GitRepository" | "HelmRepository"
70 name: string
71 namespace: string // TODO(gio): default global.id
72}
73
74#Global: {
75 id: string | *""
76 pcloudEnvName: string | *""
77 domain: string | *""
78 privateDomain: string | *""
79 namespacePrefix: string | *""
80 ...
81}
82
83#Release: {
gio3cdee592024-04-17 10:15:56 +040084 appInstanceId: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040085 namespace: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040086 repoAddr: string
87 appDir: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040088}
89
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040090#PortForward: {
91 allocator: string
92 protocol: "TCP" | "UDP" | *"TCP"
93 sourcePort: int
94 targetService: string
95 targetPort: int
96}
97
98portForward: [...#PortForward] | *[]
99
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400100global: #Global
101release: #Release
102
103_ingressPrivate: "\(global.id)-ingress-private"
104_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
105_issuerPrivate: "\(global.id)-private"
106_issuerPublic: "\(global.id)-public"
107
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400108_IngressWithAuthProxy: {
109 inp: {
110 auth: #Auth
111 network: #Network
112 subdomain: string
113 serviceName: string
114 port: { name: string } | { number: int & > 0 }
115 }
116
117 _domain: "\(inp.subdomain).\(inp.network.domain)"
118 _authProxyHTTPPortName: "http"
119
120 out: {
121 images: {
122 authProxy: #Image & {
123 repository: "giolekva"
124 name: "auth-proxy"
125 tag: "latest"
126 pullPolicy: "Always"
127 }
128 }
129 charts: {
130 ingress: #Chart & {
131 chart: "charts/ingress"
132 sourceRef: {
133 kind: "GitRepository"
134 name: "pcloud"
135 namespace: global.id
136 }
137 }
138 authProxy: #Chart & {
139 chart: "charts/auth-proxy"
140 sourceRef: {
141 kind: "GitRepository"
142 name: "pcloud"
143 namespace: global.id
144 }
145 }
146 }
147 helm: {
148 if inp.auth.enabled {
149 "auth-proxy": {
150 chart: charts.authProxy
151 values: {
152 image: {
153 repository: images.authProxy.fullName
154 tag: images.authProxy.tag
155 pullPolicy: images.authProxy.pullPolicy
156 }
157 upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
158 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
159 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400160 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400161 groups: inp.auth.groups
162 portName: _authProxyHTTPPortName
163 }
164 }
165 }
166 ingress: {
167 chart: charts.ingress
168 values: {
169 domain: _domain
170 ingressClassName: inp.network.ingressClass
171 certificateIssuer: inp.network.certificateIssuer
172 service: {
173 if inp.auth.enabled {
174 name: "auth-proxy"
175 port: name: _authProxyHTTPPortName
176 }
177 if !inp.auth.enabled {
178 name: inp.serviceName
179 if inp.port.name != _|_ {
180 port: name: inp.port.name
181 }
182 if inp.port.number != _|_ {
183 port: number: inp.port.number
184 }
185 }
186 }
187 }
188 }
189 }
190 }
191}
192
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400193images: {
194 for key, value in images {
195 "\(key)": #Image & value
196 }
197}
198
199charts: {
200 for key, value in charts {
201 "\(key)": #Chart & value
202 }
203}
204
205#ResourceReference: {
206 name: string
207 namespace: string
208}
209
210#Helm: {
211 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400212 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400213 ...
214}
215
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400216helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400217 for key, value in helm {
218 "\(key)": #Helm & value & {
219 name: key
220 }
221 }
222}
223
224#HelmRelease: {
225 _name: string
226 _chart: #Chart
227 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400228 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400229
230 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
231 kind: "HelmRelease"
232 metadata: {
233 name: _name
234 namespace: release.namespace
235 }
236 spec: {
237 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400238 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400239 chart: {
240 spec: _chart
241 }
242 values: _values
243 }
244}
245
246output: {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400247 for name, r in helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400248 "\(name)": #HelmRelease & {
249 _name: name
250 _chart: r.chart
251 _values: r.values
252 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400253 }
254 }
255}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400256
257#SSHKey: {
258 public: string
259 private: string
260}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400261`
262
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400263type Rendered struct {
gio3cdee592024-04-17 10:15:56 +0400264 Name string
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400265 Readme string
266 Resources map[string][]byte
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400267 Ports []PortForward
gio3cdee592024-04-17 10:15:56 +0400268 Config AppInstanceConfig
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400269}
270
gio3cdee592024-04-17 10:15:56 +0400271type PortForward struct {
272 Allocator string `json:"allocator"`
273 Protocol string `json:"protocol"`
274 SourcePort int `json:"sourcePort"`
275 TargetService string `json:"targetService"`
276 TargetPort int `json:"targetPort"`
277}
278
279type AppType int
280
281const (
282 AppTypeInfra AppType = iota
283 AppTypeEnv
284)
285
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400286type App interface {
gio3cdee592024-04-17 10:15:56 +0400287 Type() AppType
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400288 Name() string
289 Description() string
290 Icon() template.HTML
291 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400292 Namespace() string
gio3cdee592024-04-17 10:15:56 +0400293}
294
295type InfraConfig struct {
296 Name string `json:"pcloudEnvName"` // #TODO(gio): change to name
297 PublicIP []net.IP `json:"publicIP"`
298 InfraNamespacePrefix string `json:"namespacePrefix"`
299 InfraAdminPublicKey []byte `json:"infraAdminPublicKey"`
300}
301
302type InfraApp interface {
303 App
304 Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error)
305}
306
307// TODO(gio): rename to EnvConfig
308type AppEnvConfig struct {
309 Id string `json:"id"`
310 InfraName string `json:"pcloudEnvName"`
311 Domain string `json:"domain"`
312 PrivateDomain string `json:"privateDomain"`
313 ContactEmail string `json:"contactEmail"`
314 PublicIP []net.IP `json:"publicIP"`
315 NamespacePrefix string `json:"namespacePrefix"`
316}
317
318type EnvApp interface {
319 App
320 Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400321}
322
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400323type cueApp struct {
324 name string
325 description string
326 icon template.HTML
327 namespace string
328 schema Schema
329 cfg *cue.Value
330}
331
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400332func newCueApp(config *cue.Value) (cueApp, error) {
333 if config == nil {
334 return cueApp{}, fmt.Errorf("config not provided")
335 }
gio3cdee592024-04-17 10:15:56 +0400336 cfg := struct {
337 Name string `json:"name"`
338 Namespace string `json:"namespace"`
339 Description string `json:"description"`
340 Icon string `json:"icon"`
341 }{}
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400342 if err := config.Decode(&cfg); err != nil {
343 return cueApp{}, err
344 }
345 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
346 if err != nil {
347 return cueApp{}, err
348 }
349 return cueApp{
350 name: cfg.Name,
351 description: cfg.Description,
352 icon: template.HTML(cfg.Icon),
353 namespace: cfg.Namespace,
354 schema: schema,
355 cfg: config,
356 }, nil
357}
358
359func (a cueApp) Name() string {
360 return a.name
361}
362
363func (a cueApp) Description() string {
364 return a.description
365}
366
367func (a cueApp) Icon() template.HTML {
368 return a.icon
369}
370
371func (a cueApp) Schema() Schema {
372 return a.schema
373}
374
gioef01fbb2024-04-12 16:52:59 +0400375func (a cueApp) Namespace() string {
376 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400377}
378
gio3cdee592024-04-17 10:15:56 +0400379func (a cueApp) render(values map[string]any) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400380 ret := Rendered{
gio3cdee592024-04-17 10:15:56 +0400381 Name: a.Name(),
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400382 Resources: make(map[string][]byte),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400383 Ports: make([]PortForward, 0),
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400384 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400385 var buf bytes.Buffer
gio3cdee592024-04-17 10:15:56 +0400386 if err := json.NewEncoder(&buf).Encode(values); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400387 return Rendered{}, err
388 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400389 ctx := a.cfg.Context()
390 d := ctx.CompileBytes(buf.Bytes())
391 res := a.cfg.Unify(d).Eval()
392 if err := res.Err(); err != nil {
393 return Rendered{}, err
394 }
395 if err := res.Validate(); err != nil {
396 return Rendered{}, err
397 }
398 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
399 if err != nil {
400 return Rendered{}, err
401 }
402 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400403 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
404 return Rendered{}, err
405 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400406 output := res.LookupPath(cue.ParsePath("output"))
407 i, err := output.Fields()
408 if err != nil {
409 return Rendered{}, err
410 }
411 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400412 if contents, err := cueyaml.Encode(i.Value()); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400413 return Rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400414 } else {
415 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
416 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400417 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400418 }
419 return ret, nil
420}
421
gio3cdee592024-04-17 10:15:56 +0400422type cueEnvApp struct {
423 cueApp
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400424}
425
gio3cdee592024-04-17 10:15:56 +0400426func NewCueEnvApp(config *cue.Value) (EnvApp, error) {
427 app, err := newCueApp(config)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400428 if err != nil {
429 return nil, err
430 }
gio3cdee592024-04-17 10:15:56 +0400431 return cueEnvApp{app}, nil
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400432}
433
gio3cdee592024-04-17 10:15:56 +0400434func (a cueEnvApp) Type() AppType {
435 return AppTypeEnv
436}
437
438func (a cueEnvApp) Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error) {
439 networks := CreateNetworks(env)
440 derived, err := deriveValues(values, a.Schema(), networks)
441 if err != nil {
442 return Rendered{}, nil
443 }
444 ret, err := a.cueApp.render(map[string]any{
445 "global": env,
446 "release": release,
447 "input": derived,
448 })
449 if err != nil {
450 return Rendered{}, err
451 }
452 ret.Config = AppInstanceConfig{
453 AppId: a.Name(),
454 Env: env,
455 Release: release,
456 Values: values,
457 Input: derived,
458 }
459 return ret, nil
460}
461
462type cueInfraApp struct {
463 cueApp
464}
465
466func NewCueInfraApp(config *cue.Value) (InfraApp, error) {
467 app, err := newCueApp(config)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400468 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400469 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400470 }
gio3cdee592024-04-17 10:15:56 +0400471 return cueInfraApp{app}, nil
472}
473
474func (a cueInfraApp) Type() AppType {
475 return AppTypeInfra
476}
477
478func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error) {
479 return a.cueApp.render(map[string]any{
480 "global": infra,
481 "release": release,
482 "input": values,
483 })
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400484}
485
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400486func cleanName(s string) string {
487 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400488}