blob: 5791780b152553a49f32de84741fd1c0d42ff4a1 [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 Lekveishvili743fb432023-11-08 17:19:40 +04004 "archive/tar"
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +04005 "bytes"
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +04006 "compress/gzip"
giolekva8aa73e82022-07-09 11:34:39 +04007 "embed"
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +04008 "encoding/json"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +04009 "fmt"
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040010 template "html/template"
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040011 "io"
giolekva8aa73e82022-07-09 11:34:39 +040012 "log"
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040013 "net/http"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040014 "strings"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040015
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +040016 "cuelang.org/go/cue"
17 "cuelang.org/go/cue/cuecontext"
18 cueyaml "cuelang.org/go/encoding/yaml"
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +040019 "github.com/go-git/go-billy/v5"
20 "sigs.k8s.io/yaml"
giolekva8aa73e82022-07-09 11:34:39 +040021)
giolekva050609f2021-12-29 15:51:40 +040022
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040023//go:embed values-tmpl
24var valuesTmpls embed.FS
25
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040026var storeAppConfigs = []string{
27 "values-tmpl/jellyfin.cue",
28 // "values-tmpl/maddy.cue",
29 "values-tmpl/matrix.cue",
30 "values-tmpl/penpot.cue",
31 "values-tmpl/pihole.cue",
32 "values-tmpl/qbittorrent.cue",
33 "values-tmpl/rpuppy.cue",
34 "values-tmpl/soft-serve.cue",
35 "values-tmpl/vaultwarden.cue",
36}
37
38var infraAppConfigs = []string{
39 "values-tmpl/appmanager.cue",
40 "values-tmpl/cert-manager.cue",
41 "values-tmpl/certificate-issuer-private.cue",
42 "values-tmpl/certificate-issuer-public.cue",
43 "values-tmpl/config-repo.cue",
44 "values-tmpl/core-auth.cue",
45 "values-tmpl/csi-driver-smb.cue",
46 "values-tmpl/dns-zone-manager.cue",
47 "values-tmpl/env-manager.cue",
48 "values-tmpl/fluxcd-reconciler.cue",
49 "values-tmpl/headscale-controller.cue",
50 "values-tmpl/headscale-user.cue",
51 "values-tmpl/headscale.cue",
52 "values-tmpl/ingress-public.cue",
53 "values-tmpl/metallb-ipaddresspool.cue",
54 "values-tmpl/private-network.cue",
55 "values-tmpl/resource-renderer-controller.cue",
56 "values-tmpl/welcome.cue",
57}
58
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040059const cueBaseConfigImports = `
60import (
61 "list"
62)
63`
64
65// TODO(gio): import
66const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040067name: string | *""
68description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040069readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040070icon: string | *""
71namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040072
73#Network: {
74 name: string
75 ingressClass: string
76 certificateIssuer: string | *""
77 domain: string
78}
79
80#Image: {
81 registry: string | *"docker.io"
82 repository: string
83 name: string
84 tag: string
85 pullPolicy: string | *"IfNotPresent"
86 imageName: "\(repository)/\(name)"
87 fullName: "\(registry)/\(imageName)"
88 fullNameWithTag: "\(fullName):\(tag)"
89}
90
91#Chart: {
92 chart: string
93 sourceRef: #SourceRef
94}
95
96#SourceRef: {
97 kind: "GitRepository" | "HelmRepository"
98 name: string
99 namespace: string // TODO(gio): default global.id
100}
101
102#Global: {
103 id: string | *""
104 pcloudEnvName: string | *""
105 domain: string | *""
106 privateDomain: string | *""
107 namespacePrefix: string | *""
108 ...
109}
110
111#Release: {
112 namespace: string
113}
114
115global: #Global
116release: #Release
117
118_ingressPrivate: "\(global.id)-ingress-private"
119_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
120_issuerPrivate: "\(global.id)-private"
121_issuerPublic: "\(global.id)-public"
122
123images: {
124 for key, value in images {
125 "\(key)": #Image & value
126 }
127}
128
129charts: {
130 for key, value in charts {
131 "\(key)": #Chart & value
132 }
133}
134
135#ResourceReference: {
136 name: string
137 namespace: string
138}
139
140#Helm: {
141 name: string
142 dependsOn: [...#Helm] | *[]
143 dependsOnExternal: [...#ResourceReference] | *[]
144 ...
145}
146
147helm: {
148 for key, value in helm {
149 "\(key)": #Helm & value & {
150 name: key
151 }
152 }
153}
154
155#HelmRelease: {
156 _name: string
157 _chart: #Chart
158 _values: _
159 _dependencies: [...#Helm] | *[]
160 _externalDependencies: [...#ResourceReference] | *[]
161
162 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
163 kind: "HelmRelease"
164 metadata: {
165 name: _name
166 namespace: release.namespace
167 }
168 spec: {
169 interval: "1m0s"
170 dependsOn: list.Concat([_externalDependencies, [
171 for d in _dependencies {
172 name: d.name
173 namespace: release.namespace
174 }
175 ]])
176 chart: {
177 spec: _chart
178 }
179 values: _values
180 }
181}
182
183output: {
184 for name, r in helm {
185 "\(name)": #HelmRelease & {
186 _name: name
187 _chart: r.chart
188 _values: r.values
189 _dependencies: r.dependsOn
190 _externalDependencies: r.dependsOnExternal
191 }
192 }
193}
194`
195
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400196type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400197 Name string `json:"name"`
198 Version string `json:"version"`
199 Description string `json:"description"`
200 Namespaces []string `json:"namespaces"`
201 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400202}
203
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400204type Rendered struct {
205 Readme string
206 Resources map[string][]byte
207}
208
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400209type App interface {
210 Name() string
211 Description() string
212 Icon() template.HTML
213 Schema() Schema
214 Namespaces() []string
215 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400216}
217
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400218type cueApp struct {
219 name string
220 description string
221 icon template.HTML
222 namespace string
223 schema Schema
224 cfg *cue.Value
225}
226
227type cueAppConfig struct {
228 Name string `json:"name"`
229 Namespace string `json:"namespace"`
230 Description string `json:"description"`
231 Icon string `json:"icon"`
232}
233
234func newCueApp(config *cue.Value) (cueApp, error) {
235 if config == nil {
236 return cueApp{}, fmt.Errorf("config not provided")
237 }
238 var cfg cueAppConfig
239 if err := config.Decode(&cfg); err != nil {
240 return cueApp{}, err
241 }
242 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
243 if err != nil {
244 return cueApp{}, err
245 }
246 return cueApp{
247 name: cfg.Name,
248 description: cfg.Description,
249 icon: template.HTML(cfg.Icon),
250 namespace: cfg.Namespace,
251 schema: schema,
252 cfg: config,
253 }, nil
254}
255
256func (a cueApp) Name() string {
257 return a.name
258}
259
260func (a cueApp) Description() string {
261 return a.description
262}
263
264func (a cueApp) Icon() template.HTML {
265 return a.icon
266}
267
268func (a cueApp) Schema() Schema {
269 return a.schema
270}
271
272func (a cueApp) Namespaces() []string {
273 return []string{a.namespace}
274}
275
276func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400277 ret := Rendered{
278 Resources: make(map[string][]byte),
279 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400280 var buf bytes.Buffer
281 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400282 return Rendered{}, err
283 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400284 ctx := a.cfg.Context()
285 d := ctx.CompileBytes(buf.Bytes())
286 res := a.cfg.Unify(d).Eval()
287 if err := res.Err(); err != nil {
288 return Rendered{}, err
289 }
290 if err := res.Validate(); err != nil {
291 return Rendered{}, err
292 }
293 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
294 if err != nil {
295 return Rendered{}, err
296 }
297 ret.Readme = readme
298 output := res.LookupPath(cue.ParsePath("output"))
299 i, err := output.Fields()
300 if err != nil {
301 return Rendered{}, err
302 }
303 for i.Next() {
304 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
305 contents, err := cueyaml.Encode(i.Value())
306 if err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400307 return Rendered{}, err
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400308 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400309 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400310 }
311 return ret, nil
312}
313
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400314type AppRepository interface {
315 GetAll() ([]App, error)
316 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400317}
318
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400319type InMemoryAppRepository struct {
320 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400321}
322
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400323func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
324 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400325}
326
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400327func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400328 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400329 if a.Name() == name {
330 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400331 }
332 }
333 return nil, fmt.Errorf("Application not found: %s", name)
334}
giolekva8aa73e82022-07-09 11:34:39 +0400335
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400336func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400337 return r.apps, nil
338}
339
giolekva8aa73e82022-07-09 11:34:39 +0400340func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400341 return append(
342 createApps(infraAppConfigs),
343 CreateStoreApps()...,
344 )
345}
346
347func CreateStoreApps() []App {
348 return createApps(storeAppConfigs)
349}
350
351func createApps(configs []string) []App {
352 ret := make([]App, len(configs))
353 for _, cfgFile := range configs {
354 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
355 if err != nil {
356 panic(err)
357 }
358 if app, err := newCueApp(cfg); err != nil {
359 panic(err)
360 } else {
361 ret = append(ret, app)
362 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400363 }
364 return ret
365}
366
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400367// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
368// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
369// if err != nil {
370// panic(err)
371// }
372// return StoreApp{
373// App{
374// "maddy",
375// []string{"app-maddy"},
376// []*template.Template{
377// tmpls.Lookup("maddy.yaml"),
378// },
379// schema,
380// nil,
381// nil,
382// },
383// `<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M9.5 13c13.687 13.574 14.825 13.09 29 0"/><rect width="37" height="31" x="5.5" y="8.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" rx="2"/></svg>`,
384// "SMPT/IMAP server to communicate via email.",
385// }
386// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400387
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400388type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400389 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400390}
391
392type appVersion struct {
393 Version string `json:"version"`
394 Urls []string `json:"urls"`
395}
396
397type allAppsResp struct {
398 ApiVersion string `json:"apiVersion"`
399 Entries map[string][]appVersion `json:"entries"`
400}
401
402func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
403 resp, err := http.Get(addr)
404 if err != nil {
405 return err
406 }
407 b, err := io.ReadAll(resp.Body)
408 if err != nil {
409 return err
410 }
411 var apps allAppsResp
412 if err := yaml.Unmarshal(b, &apps); err != nil {
413 return err
414 }
415 for name, conf := range apps.Entries {
416 for _, version := range conf {
417 resp, err := http.Get(version.Urls[0])
418 if err != nil {
419 return err
420 }
421 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
422 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
423 return err
424 }
425 sub, err := fs.Chroot(nameVersion)
426 if err != nil {
427 return err
428 }
429 if err := extractApp(resp.Body, sub); err != nil {
430 return err
431 }
432 }
433 }
434 return nil
435}
436
437func extractApp(archive io.Reader, fs billy.Filesystem) error {
438 uncompressed, err := gzip.NewReader(archive)
439 if err != nil {
440 return err
441 }
442 tarReader := tar.NewReader(uncompressed)
443 for true {
444 header, err := tarReader.Next()
445 if err == io.EOF {
446 break
447 }
448 if err != nil {
449 return err
450 }
451 switch header.Typeflag {
452 case tar.TypeDir:
453 if err := fs.MkdirAll(header.Name, 0755); err != nil {
454 return err
455 }
456 case tar.TypeReg:
457 out, err := fs.Create(header.Name)
458 if err != nil {
459 return err
460 }
461 defer out.Close()
462 if _, err := io.Copy(out, tarReader); err != nil {
463 return err
464 }
465 default:
466 return fmt.Errorf("Uknown type: %s", header.Name)
467 }
468 }
469 return nil
470}
471
472type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400473 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400474 fs billy.Filesystem
475}
476
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400477func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400478 all, err := fs.ReadDir(".")
479 if err != nil {
480 return nil, err
481 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400482 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400483 for _, e := range all {
484 if !e.IsDir() {
485 continue
486 }
487 appFS, err := fs.Chroot(e.Name())
488 if err != nil {
489 return nil, err
490 }
491 app, err := loadApp(appFS)
492 if err != nil {
493 log.Printf("Ignoring directory %s: %s", e.Name(), err)
494 continue
495 }
496 apps = append(apps, app)
497 }
498 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400499 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400500 fs,
501 }, nil
502}
503
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400504func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400505 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400506 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400507 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400508 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400509 var contents bytes.Buffer
510 for _, i := range items {
511 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400512 continue
513 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400514 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400515 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400516 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400517 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400518 defer f.Close()
519 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400520 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400521 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400522 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400523 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400524 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400525 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400526 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400527 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400528}
529
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400530func cleanName(s string) string {
531 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400532}
533
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400534func processCueConfig(contents string) (*cue.Value, error) {
535 ctx := cuecontext.New()
536 cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
537 if err := cfg.Err(); err != nil {
538 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400539 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400540 if err := cfg.Validate(); err != nil {
541 return nil, err
542 }
543 return &cfg, nil
544}
545
546func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
547 contents, err := fs.ReadFile(f)
548 if err != nil {
549 return nil, err
550 }
551 return processCueConfig(string(contents))
552}
553
554func createApp(fs embed.FS, configFile string) App {
555 cfg, err := readCueConfigFromFile(fs, configFile)
556 if err != nil {
557 panic(err)
558 }
559 if app, err := newCueApp(cfg); err != nil {
560 panic(err)
561 } else {
562 return app
563 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400564}