blob: ac83a6659993d61683276feda5521fd5cc082fe7 [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",
Giorgi Lekveishvili925f0de2024-03-14 18:51:56 +040058 "values-tmpl/hydra-maester.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040059}
60
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040061const cueBaseConfigImports = `
62import (
63 "list"
64)
65`
66
67// TODO(gio): import
68const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040069name: string | *""
70description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040071readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040072icon: string | *""
73namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040074
75#Network: {
76 name: string
77 ingressClass: string
78 certificateIssuer: string | *""
79 domain: string
80}
81
82#Image: {
83 registry: string | *"docker.io"
84 repository: string
85 name: string
86 tag: string
87 pullPolicy: string | *"IfNotPresent"
88 imageName: "\(repository)/\(name)"
89 fullName: "\(registry)/\(imageName)"
90 fullNameWithTag: "\(fullName):\(tag)"
91}
92
93#Chart: {
94 chart: string
95 sourceRef: #SourceRef
96}
97
98#SourceRef: {
99 kind: "GitRepository" | "HelmRepository"
100 name: string
101 namespace: string // TODO(gio): default global.id
102}
103
104#Global: {
105 id: string | *""
106 pcloudEnvName: string | *""
107 domain: string | *""
108 privateDomain: string | *""
109 namespacePrefix: string | *""
110 ...
111}
112
113#Release: {
114 namespace: string
115}
116
117global: #Global
118release: #Release
119
120_ingressPrivate: "\(global.id)-ingress-private"
121_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
122_issuerPrivate: "\(global.id)-private"
123_issuerPublic: "\(global.id)-public"
124
125images: {
126 for key, value in images {
127 "\(key)": #Image & value
128 }
129}
130
131charts: {
132 for key, value in charts {
133 "\(key)": #Chart & value
134 }
135}
136
137#ResourceReference: {
138 name: string
139 namespace: string
140}
141
142#Helm: {
143 name: string
144 dependsOn: [...#Helm] | *[]
145 dependsOnExternal: [...#ResourceReference] | *[]
146 ...
147}
148
149helm: {
150 for key, value in helm {
151 "\(key)": #Helm & value & {
152 name: key
153 }
154 }
155}
156
157#HelmRelease: {
158 _name: string
159 _chart: #Chart
160 _values: _
161 _dependencies: [...#Helm] | *[]
162 _externalDependencies: [...#ResourceReference] | *[]
163
164 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
165 kind: "HelmRelease"
166 metadata: {
167 name: _name
168 namespace: release.namespace
169 }
170 spec: {
171 interval: "1m0s"
172 dependsOn: list.Concat([_externalDependencies, [
173 for d in _dependencies {
174 name: d.name
175 namespace: release.namespace
176 }
177 ]])
178 chart: {
179 spec: _chart
180 }
181 values: _values
182 }
183}
184
185output: {
186 for name, r in helm {
187 "\(name)": #HelmRelease & {
188 _name: name
189 _chart: r.chart
190 _values: r.values
191 _dependencies: r.dependsOn
192 _externalDependencies: r.dependsOnExternal
193 }
194 }
195}
196`
197
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400198type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400199 Name string `json:"name"`
200 Version string `json:"version"`
201 Description string `json:"description"`
202 Namespaces []string `json:"namespaces"`
203 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400204}
205
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400206type Rendered struct {
207 Readme string
208 Resources map[string][]byte
209}
210
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400211type App interface {
212 Name() string
213 Description() string
214 Icon() template.HTML
215 Schema() Schema
216 Namespaces() []string
217 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400218}
219
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400220type cueApp struct {
221 name string
222 description string
223 icon template.HTML
224 namespace string
225 schema Schema
226 cfg *cue.Value
227}
228
229type cueAppConfig struct {
230 Name string `json:"name"`
231 Namespace string `json:"namespace"`
232 Description string `json:"description"`
233 Icon string `json:"icon"`
234}
235
236func newCueApp(config *cue.Value) (cueApp, error) {
237 if config == nil {
238 return cueApp{}, fmt.Errorf("config not provided")
239 }
240 var cfg cueAppConfig
241 if err := config.Decode(&cfg); err != nil {
242 return cueApp{}, err
243 }
244 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
245 if err != nil {
246 return cueApp{}, err
247 }
248 return cueApp{
249 name: cfg.Name,
250 description: cfg.Description,
251 icon: template.HTML(cfg.Icon),
252 namespace: cfg.Namespace,
253 schema: schema,
254 cfg: config,
255 }, nil
256}
257
258func (a cueApp) Name() string {
259 return a.name
260}
261
262func (a cueApp) Description() string {
263 return a.description
264}
265
266func (a cueApp) Icon() template.HTML {
267 return a.icon
268}
269
270func (a cueApp) Schema() Schema {
271 return a.schema
272}
273
274func (a cueApp) Namespaces() []string {
275 return []string{a.namespace}
276}
277
278func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400279 ret := Rendered{
280 Resources: make(map[string][]byte),
281 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400282 var buf bytes.Buffer
283 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400284 return Rendered{}, err
285 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400286 ctx := a.cfg.Context()
287 d := ctx.CompileBytes(buf.Bytes())
288 res := a.cfg.Unify(d).Eval()
289 if err := res.Err(); err != nil {
290 return Rendered{}, err
291 }
292 if err := res.Validate(); err != nil {
293 return Rendered{}, err
294 }
295 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
296 if err != nil {
297 return Rendered{}, err
298 }
299 ret.Readme = readme
300 output := res.LookupPath(cue.ParsePath("output"))
301 i, err := output.Fields()
302 if err != nil {
303 return Rendered{}, err
304 }
305 for i.Next() {
306 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
307 contents, err := cueyaml.Encode(i.Value())
308 if err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400309 return Rendered{}, err
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400310 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400311 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400312 }
313 return ret, nil
314}
315
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400316type AppRepository interface {
317 GetAll() ([]App, error)
318 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400319}
320
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400321type InMemoryAppRepository struct {
322 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400323}
324
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400325func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
326 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400327}
328
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400329func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400330 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400331 if a.Name() == name {
332 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400333 }
334 }
335 return nil, fmt.Errorf("Application not found: %s", name)
336}
giolekva8aa73e82022-07-09 11:34:39 +0400337
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400338func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400339 return r.apps, nil
340}
341
giolekva8aa73e82022-07-09 11:34:39 +0400342func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400343 return append(
344 createApps(infraAppConfigs),
345 CreateStoreApps()...,
346 )
347}
348
349func CreateStoreApps() []App {
350 return createApps(storeAppConfigs)
351}
352
353func createApps(configs []string) []App {
Giorgi Lekveishvili186eae52024-02-15 14:21:41 +0400354 ret := make([]App, 0)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400355 for _, cfgFile := range configs {
356 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
357 if err != nil {
358 panic(err)
359 }
360 if app, err := newCueApp(cfg); err != nil {
361 panic(err)
362 } else {
363 ret = append(ret, app)
364 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400365 }
366 return ret
367}
368
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400369// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
370// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
371// if err != nil {
372// panic(err)
373// }
374// return StoreApp{
375// App{
376// "maddy",
377// []string{"app-maddy"},
378// []*template.Template{
379// tmpls.Lookup("maddy.yaml"),
380// },
381// schema,
382// nil,
383// nil,
384// },
385// `<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>`,
386// "SMPT/IMAP server to communicate via email.",
387// }
388// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400389
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400390type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400391 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400392}
393
394type appVersion struct {
395 Version string `json:"version"`
396 Urls []string `json:"urls"`
397}
398
399type allAppsResp struct {
400 ApiVersion string `json:"apiVersion"`
401 Entries map[string][]appVersion `json:"entries"`
402}
403
404func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
405 resp, err := http.Get(addr)
406 if err != nil {
407 return err
408 }
409 b, err := io.ReadAll(resp.Body)
410 if err != nil {
411 return err
412 }
413 var apps allAppsResp
414 if err := yaml.Unmarshal(b, &apps); err != nil {
415 return err
416 }
417 for name, conf := range apps.Entries {
418 for _, version := range conf {
419 resp, err := http.Get(version.Urls[0])
420 if err != nil {
421 return err
422 }
423 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
424 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
425 return err
426 }
427 sub, err := fs.Chroot(nameVersion)
428 if err != nil {
429 return err
430 }
431 if err := extractApp(resp.Body, sub); err != nil {
432 return err
433 }
434 }
435 }
436 return nil
437}
438
439func extractApp(archive io.Reader, fs billy.Filesystem) error {
440 uncompressed, err := gzip.NewReader(archive)
441 if err != nil {
442 return err
443 }
444 tarReader := tar.NewReader(uncompressed)
445 for true {
446 header, err := tarReader.Next()
447 if err == io.EOF {
448 break
449 }
450 if err != nil {
451 return err
452 }
453 switch header.Typeflag {
454 case tar.TypeDir:
455 if err := fs.MkdirAll(header.Name, 0755); err != nil {
456 return err
457 }
458 case tar.TypeReg:
459 out, err := fs.Create(header.Name)
460 if err != nil {
461 return err
462 }
463 defer out.Close()
464 if _, err := io.Copy(out, tarReader); err != nil {
465 return err
466 }
467 default:
468 return fmt.Errorf("Uknown type: %s", header.Name)
469 }
470 }
471 return nil
472}
473
474type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400475 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400476 fs billy.Filesystem
477}
478
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400479func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400480 all, err := fs.ReadDir(".")
481 if err != nil {
482 return nil, err
483 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400484 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400485 for _, e := range all {
486 if !e.IsDir() {
487 continue
488 }
489 appFS, err := fs.Chroot(e.Name())
490 if err != nil {
491 return nil, err
492 }
493 app, err := loadApp(appFS)
494 if err != nil {
495 log.Printf("Ignoring directory %s: %s", e.Name(), err)
496 continue
497 }
498 apps = append(apps, app)
499 }
500 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400501 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400502 fs,
503 }, nil
504}
505
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400506func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400507 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400508 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400509 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400510 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400511 var contents bytes.Buffer
512 for _, i := range items {
513 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400514 continue
515 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400516 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400517 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400518 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400519 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400520 defer f.Close()
521 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400522 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400523 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400524 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400525 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400526 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400527 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400528 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400529 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400530}
531
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400532func cleanName(s string) string {
533 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400534}
535
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400536func processCueConfig(contents string) (*cue.Value, error) {
537 ctx := cuecontext.New()
538 cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
539 if err := cfg.Err(); err != nil {
540 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400541 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400542 if err := cfg.Validate(); err != nil {
543 return nil, err
544 }
545 return &cfg, nil
546}
547
548func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
549 contents, err := fs.ReadFile(f)
550 if err != nil {
551 return nil, err
552 }
553 return processCueConfig(string(contents))
554}
555
556func createApp(fs embed.FS, configFile string) App {
557 cfg, err := readCueConfigFromFile(fs, configFile)
558 if err != nil {
559 panic(err)
560 }
561 if app, err := newCueApp(cfg); err != nil {
562 panic(err)
563 } else {
564 return app
565 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400566}