blob: fa2e27079e7cfba5f17b871965342e029b560ccf [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",
DTabidze09935812024-03-13 13:47:39 +040036 "values-tmpl/url-shortener.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040037}
38
39var infraAppConfigs = []string{
40 "values-tmpl/appmanager.cue",
41 "values-tmpl/cert-manager.cue",
42 "values-tmpl/certificate-issuer-private.cue",
43 "values-tmpl/certificate-issuer-public.cue",
44 "values-tmpl/config-repo.cue",
45 "values-tmpl/core-auth.cue",
46 "values-tmpl/csi-driver-smb.cue",
47 "values-tmpl/dns-zone-manager.cue",
48 "values-tmpl/env-manager.cue",
49 "values-tmpl/fluxcd-reconciler.cue",
50 "values-tmpl/headscale-controller.cue",
51 "values-tmpl/headscale-user.cue",
52 "values-tmpl/headscale.cue",
53 "values-tmpl/ingress-public.cue",
54 "values-tmpl/metallb-ipaddresspool.cue",
55 "values-tmpl/private-network.cue",
56 "values-tmpl/resource-renderer-controller.cue",
57 "values-tmpl/welcome.cue",
58}
59
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040060const cueBaseConfigImports = `
61import (
62 "list"
63)
64`
65
66// TODO(gio): import
67const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040068name: string | *""
69description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040070readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040071icon: string | *""
72namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040073
74#Network: {
75 name: string
76 ingressClass: string
77 certificateIssuer: string | *""
78 domain: string
79}
80
81#Image: {
82 registry: string | *"docker.io"
83 repository: string
84 name: string
85 tag: string
86 pullPolicy: string | *"IfNotPresent"
87 imageName: "\(repository)/\(name)"
88 fullName: "\(registry)/\(imageName)"
89 fullNameWithTag: "\(fullName):\(tag)"
90}
91
92#Chart: {
93 chart: string
94 sourceRef: #SourceRef
95}
96
97#SourceRef: {
98 kind: "GitRepository" | "HelmRepository"
99 name: string
100 namespace: string // TODO(gio): default global.id
101}
102
103#Global: {
104 id: string | *""
105 pcloudEnvName: string | *""
106 domain: string | *""
107 privateDomain: string | *""
108 namespacePrefix: string | *""
109 ...
110}
111
112#Release: {
113 namespace: string
114}
115
116global: #Global
117release: #Release
118
119_ingressPrivate: "\(global.id)-ingress-private"
120_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
121_issuerPrivate: "\(global.id)-private"
122_issuerPublic: "\(global.id)-public"
123
124images: {
125 for key, value in images {
126 "\(key)": #Image & value
127 }
128}
129
130charts: {
131 for key, value in charts {
132 "\(key)": #Chart & value
133 }
134}
135
136#ResourceReference: {
137 name: string
138 namespace: string
139}
140
141#Helm: {
142 name: string
143 dependsOn: [...#Helm] | *[]
144 dependsOnExternal: [...#ResourceReference] | *[]
145 ...
146}
147
148helm: {
149 for key, value in helm {
150 "\(key)": #Helm & value & {
151 name: key
152 }
153 }
154}
155
156#HelmRelease: {
157 _name: string
158 _chart: #Chart
159 _values: _
160 _dependencies: [...#Helm] | *[]
161 _externalDependencies: [...#ResourceReference] | *[]
162
163 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
164 kind: "HelmRelease"
165 metadata: {
166 name: _name
167 namespace: release.namespace
168 }
169 spec: {
170 interval: "1m0s"
171 dependsOn: list.Concat([_externalDependencies, [
172 for d in _dependencies {
173 name: d.name
174 namespace: release.namespace
175 }
176 ]])
177 chart: {
178 spec: _chart
179 }
180 values: _values
181 }
182}
183
184output: {
185 for name, r in helm {
186 "\(name)": #HelmRelease & {
187 _name: name
188 _chart: r.chart
189 _values: r.values
190 _dependencies: r.dependsOn
191 _externalDependencies: r.dependsOnExternal
192 }
193 }
194}
195`
196
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400197type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400198 Name string `json:"name"`
199 Version string `json:"version"`
200 Description string `json:"description"`
201 Namespaces []string `json:"namespaces"`
202 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400203}
204
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400205type Rendered struct {
206 Readme string
207 Resources map[string][]byte
208}
209
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400210type App interface {
211 Name() string
212 Description() string
213 Icon() template.HTML
214 Schema() Schema
215 Namespaces() []string
216 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400217}
218
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400219type cueApp struct {
220 name string
221 description string
222 icon template.HTML
223 namespace string
224 schema Schema
225 cfg *cue.Value
226}
227
228type cueAppConfig struct {
229 Name string `json:"name"`
230 Namespace string `json:"namespace"`
231 Description string `json:"description"`
232 Icon string `json:"icon"`
233}
234
235func newCueApp(config *cue.Value) (cueApp, error) {
236 if config == nil {
237 return cueApp{}, fmt.Errorf("config not provided")
238 }
239 var cfg cueAppConfig
240 if err := config.Decode(&cfg); err != nil {
241 return cueApp{}, err
242 }
243 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
244 if err != nil {
245 return cueApp{}, err
246 }
247 return cueApp{
248 name: cfg.Name,
249 description: cfg.Description,
250 icon: template.HTML(cfg.Icon),
251 namespace: cfg.Namespace,
252 schema: schema,
253 cfg: config,
254 }, nil
255}
256
257func (a cueApp) Name() string {
258 return a.name
259}
260
261func (a cueApp) Description() string {
262 return a.description
263}
264
265func (a cueApp) Icon() template.HTML {
266 return a.icon
267}
268
269func (a cueApp) Schema() Schema {
270 return a.schema
271}
272
273func (a cueApp) Namespaces() []string {
274 return []string{a.namespace}
275}
276
277func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400278 ret := Rendered{
279 Resources: make(map[string][]byte),
280 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400281 var buf bytes.Buffer
282 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400283 return Rendered{}, err
284 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400285 ctx := a.cfg.Context()
286 d := ctx.CompileBytes(buf.Bytes())
287 res := a.cfg.Unify(d).Eval()
288 if err := res.Err(); err != nil {
289 return Rendered{}, err
290 }
291 if err := res.Validate(); err != nil {
292 return Rendered{}, err
293 }
294 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
295 if err != nil {
296 return Rendered{}, err
297 }
298 ret.Readme = readme
299 output := res.LookupPath(cue.ParsePath("output"))
300 i, err := output.Fields()
301 if err != nil {
302 return Rendered{}, err
303 }
304 for i.Next() {
305 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
306 contents, err := cueyaml.Encode(i.Value())
307 if err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400308 return Rendered{}, err
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400309 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400310 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400311 }
312 return ret, nil
313}
314
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400315type AppRepository interface {
316 GetAll() ([]App, error)
317 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400318}
319
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400320type InMemoryAppRepository struct {
321 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400322}
323
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400324func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
325 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400326}
327
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400328func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400329 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400330 if a.Name() == name {
331 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400332 }
333 }
334 return nil, fmt.Errorf("Application not found: %s", name)
335}
giolekva8aa73e82022-07-09 11:34:39 +0400336
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400337func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400338 return r.apps, nil
339}
340
giolekva8aa73e82022-07-09 11:34:39 +0400341func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400342 return append(
343 createApps(infraAppConfigs),
344 CreateStoreApps()...,
345 )
346}
347
348func CreateStoreApps() []App {
349 return createApps(storeAppConfigs)
350}
351
352func createApps(configs []string) []App {
Giorgi Lekveishvili186eae52024-02-15 14:21:41 +0400353 ret := make([]App, 0)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400354 for _, cfgFile := range configs {
355 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
356 if err != nil {
357 panic(err)
358 }
359 if app, err := newCueApp(cfg); err != nil {
360 panic(err)
361 } else {
362 ret = append(ret, app)
363 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400364 }
365 return ret
366}
367
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400368// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
369// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
370// if err != nil {
371// panic(err)
372// }
373// return StoreApp{
374// App{
375// "maddy",
376// []string{"app-maddy"},
377// []*template.Template{
378// tmpls.Lookup("maddy.yaml"),
379// },
380// schema,
381// nil,
382// nil,
383// },
384// `<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>`,
385// "SMPT/IMAP server to communicate via email.",
386// }
387// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400388
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400389type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400390 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400391}
392
393type appVersion struct {
394 Version string `json:"version"`
395 Urls []string `json:"urls"`
396}
397
398type allAppsResp struct {
399 ApiVersion string `json:"apiVersion"`
400 Entries map[string][]appVersion `json:"entries"`
401}
402
403func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
404 resp, err := http.Get(addr)
405 if err != nil {
406 return err
407 }
408 b, err := io.ReadAll(resp.Body)
409 if err != nil {
410 return err
411 }
412 var apps allAppsResp
413 if err := yaml.Unmarshal(b, &apps); err != nil {
414 return err
415 }
416 for name, conf := range apps.Entries {
417 for _, version := range conf {
418 resp, err := http.Get(version.Urls[0])
419 if err != nil {
420 return err
421 }
422 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
423 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
424 return err
425 }
426 sub, err := fs.Chroot(nameVersion)
427 if err != nil {
428 return err
429 }
430 if err := extractApp(resp.Body, sub); err != nil {
431 return err
432 }
433 }
434 }
435 return nil
436}
437
438func extractApp(archive io.Reader, fs billy.Filesystem) error {
439 uncompressed, err := gzip.NewReader(archive)
440 if err != nil {
441 return err
442 }
443 tarReader := tar.NewReader(uncompressed)
444 for true {
445 header, err := tarReader.Next()
446 if err == io.EOF {
447 break
448 }
449 if err != nil {
450 return err
451 }
452 switch header.Typeflag {
453 case tar.TypeDir:
454 if err := fs.MkdirAll(header.Name, 0755); err != nil {
455 return err
456 }
457 case tar.TypeReg:
458 out, err := fs.Create(header.Name)
459 if err != nil {
460 return err
461 }
462 defer out.Close()
463 if _, err := io.Copy(out, tarReader); err != nil {
464 return err
465 }
466 default:
467 return fmt.Errorf("Uknown type: %s", header.Name)
468 }
469 }
470 return nil
471}
472
473type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400474 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400475 fs billy.Filesystem
476}
477
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400478func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400479 all, err := fs.ReadDir(".")
480 if err != nil {
481 return nil, err
482 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400483 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400484 for _, e := range all {
485 if !e.IsDir() {
486 continue
487 }
488 appFS, err := fs.Chroot(e.Name())
489 if err != nil {
490 return nil, err
491 }
492 app, err := loadApp(appFS)
493 if err != nil {
494 log.Printf("Ignoring directory %s: %s", e.Name(), err)
495 continue
496 }
497 apps = append(apps, app)
498 }
499 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400500 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400501 fs,
502 }, nil
503}
504
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400505func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400506 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400507 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400508 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400509 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400510 var contents bytes.Buffer
511 for _, i := range items {
512 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400513 continue
514 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400515 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400516 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400517 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400518 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400519 defer f.Close()
520 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400521 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400522 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400523 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400524 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400525 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400526 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400527 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400528 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400529}
530
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400531func cleanName(s string) string {
532 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400533}
534
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400535func processCueConfig(contents string) (*cue.Value, error) {
536 ctx := cuecontext.New()
537 cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
538 if err := cfg.Err(); err != nil {
539 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400540 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400541 if err := cfg.Validate(); err != nil {
542 return nil, err
543 }
544 return &cfg, nil
545}
546
547func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
548 contents, err := fs.ReadFile(f)
549 if err != nil {
550 return nil, err
551 }
552 return processCueConfig(string(contents))
553}
554
555func createApp(fs embed.FS, configFile string) App {
556 cfg, err := readCueConfigFromFile(fs, configFile)
557 if err != nil {
558 panic(err)
559 }
560 if app, err := newCueApp(cfg); err != nil {
561 panic(err)
562 } else {
563 return app
564 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400565}