blob: b710acc9dac2372552c25fc8195016ac95579efa [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 Lekveishviliee15ee22024-03-28 12:35:10 +040037 "values-tmpl/gerrit.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040038}
39
40var infraAppConfigs = []string{
41 "values-tmpl/appmanager.cue",
42 "values-tmpl/cert-manager.cue",
43 "values-tmpl/certificate-issuer-private.cue",
44 "values-tmpl/certificate-issuer-public.cue",
45 "values-tmpl/config-repo.cue",
46 "values-tmpl/core-auth.cue",
47 "values-tmpl/csi-driver-smb.cue",
48 "values-tmpl/dns-zone-manager.cue",
49 "values-tmpl/env-manager.cue",
50 "values-tmpl/fluxcd-reconciler.cue",
51 "values-tmpl/headscale-controller.cue",
52 "values-tmpl/headscale-user.cue",
53 "values-tmpl/headscale.cue",
54 "values-tmpl/ingress-public.cue",
55 "values-tmpl/metallb-ipaddresspool.cue",
56 "values-tmpl/private-network.cue",
57 "values-tmpl/resource-renderer-controller.cue",
58 "values-tmpl/welcome.cue",
DTabidze0d802592024-03-19 17:42:45 +040059 "values-tmpl/memberships.cue",
Giorgi Lekveishvili925f0de2024-03-14 18:51:56 +040060 "values-tmpl/hydra-maester.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040061}
62
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040063// TODO(gio): import
64const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040065name: string | *""
66description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040067readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040068icon: string | *""
69namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040070
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040071#Auth: {
72 enabled: bool | *false // TODO(gio): enabled by default?
73 groups: string | *"" // TODO(gio): []string
74}
75
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040076#Network: {
77 name: string
78 ingressClass: string
79 certificateIssuer: string | *""
80 domain: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040081 allocatePortAddr: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040082}
83
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040084networks: {
85 public: #Network & {
86 name: "Public"
87 ingressClass: "\(global.pcloudEnvName)-ingress-public"
88 certificateIssuer: "\(global.id)-public"
89 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040090 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040091 }
92 private: #Network & {
93 name: "Private"
94 ingressClass: "\(global.id)-ingress-private"
95 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040096 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040097 }
98}
99
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400100#Image: {
101 registry: string | *"docker.io"
102 repository: string
103 name: string
104 tag: string
105 pullPolicy: string | *"IfNotPresent"
106 imageName: "\(repository)/\(name)"
107 fullName: "\(registry)/\(imageName)"
108 fullNameWithTag: "\(fullName):\(tag)"
109}
110
111#Chart: {
112 chart: string
113 sourceRef: #SourceRef
114}
115
116#SourceRef: {
117 kind: "GitRepository" | "HelmRepository"
118 name: string
119 namespace: string // TODO(gio): default global.id
120}
121
122#Global: {
123 id: string | *""
124 pcloudEnvName: string | *""
125 domain: string | *""
126 privateDomain: string | *""
127 namespacePrefix: string | *""
128 ...
129}
130
131#Release: {
132 namespace: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400133 repoAddr: string
134 appDir: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400135}
136
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400137#PortForward: {
138 allocator: string
139 protocol: "TCP" | "UDP" | *"TCP"
140 sourcePort: int
141 targetService: string
142 targetPort: int
143}
144
145portForward: [...#PortForward] | *[]
146
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400147global: #Global
148release: #Release
149
150_ingressPrivate: "\(global.id)-ingress-private"
151_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
152_issuerPrivate: "\(global.id)-private"
153_issuerPublic: "\(global.id)-public"
154
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400155_IngressWithAuthProxy: {
156 inp: {
157 auth: #Auth
158 network: #Network
159 subdomain: string
160 serviceName: string
161 port: { name: string } | { number: int & > 0 }
162 }
163
164 _domain: "\(inp.subdomain).\(inp.network.domain)"
165 _authProxyHTTPPortName: "http"
166
167 out: {
168 images: {
169 authProxy: #Image & {
170 repository: "giolekva"
171 name: "auth-proxy"
172 tag: "latest"
173 pullPolicy: "Always"
174 }
175 }
176 charts: {
177 ingress: #Chart & {
178 chart: "charts/ingress"
179 sourceRef: {
180 kind: "GitRepository"
181 name: "pcloud"
182 namespace: global.id
183 }
184 }
185 authProxy: #Chart & {
186 chart: "charts/auth-proxy"
187 sourceRef: {
188 kind: "GitRepository"
189 name: "pcloud"
190 namespace: global.id
191 }
192 }
193 }
194 helm: {
195 if inp.auth.enabled {
196 "auth-proxy": {
197 chart: charts.authProxy
198 values: {
199 image: {
200 repository: images.authProxy.fullName
201 tag: images.authProxy.tag
202 pullPolicy: images.authProxy.pullPolicy
203 }
204 upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
205 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
206 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400207 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400208 groups: inp.auth.groups
209 portName: _authProxyHTTPPortName
210 }
211 }
212 }
213 ingress: {
214 chart: charts.ingress
215 values: {
216 domain: _domain
217 ingressClassName: inp.network.ingressClass
218 certificateIssuer: inp.network.certificateIssuer
219 service: {
220 if inp.auth.enabled {
221 name: "auth-proxy"
222 port: name: _authProxyHTTPPortName
223 }
224 if !inp.auth.enabled {
225 name: inp.serviceName
226 if inp.port.name != _|_ {
227 port: name: inp.port.name
228 }
229 if inp.port.number != _|_ {
230 port: number: inp.port.number
231 }
232 }
233 }
234 }
235 }
236 }
237 }
238}
239
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400240images: {
241 for key, value in images {
242 "\(key)": #Image & value
243 }
244}
245
246charts: {
247 for key, value in charts {
248 "\(key)": #Chart & value
249 }
250}
251
252#ResourceReference: {
253 name: string
254 namespace: string
255}
256
257#Helm: {
258 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400259 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400260 ...
261}
262
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400263helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400264 for key, value in helm {
265 "\(key)": #Helm & value & {
266 name: key
267 }
268 }
269}
270
271#HelmRelease: {
272 _name: string
273 _chart: #Chart
274 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400275 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400276
277 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
278 kind: "HelmRelease"
279 metadata: {
280 name: _name
281 namespace: release.namespace
282 }
283 spec: {
284 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400285 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400286 chart: {
287 spec: _chart
288 }
289 values: _values
290 }
291}
292
293output: {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400294 for name, r in helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400295 "\(name)": #HelmRelease & {
296 _name: name
297 _chart: r.chart
298 _values: r.values
299 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400300 }
301 }
302}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400303
304#SSHKey: {
305 public: string
306 private: string
307}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400308`
309
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400310type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400311 Name string `json:"name"`
312 Version string `json:"version"`
313 Description string `json:"description"`
314 Namespaces []string `json:"namespaces"`
315 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400316}
317
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400318type Rendered struct {
319 Readme string
320 Resources map[string][]byte
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400321 Ports []PortForward
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400322}
323
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400324type App interface {
325 Name() string
326 Description() string
327 Icon() template.HTML
328 Schema() Schema
329 Namespaces() []string
330 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400331}
332
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400333type cueApp struct {
334 name string
335 description string
336 icon template.HTML
337 namespace string
338 schema Schema
339 cfg *cue.Value
340}
341
342type cueAppConfig struct {
343 Name string `json:"name"`
344 Namespace string `json:"namespace"`
345 Description string `json:"description"`
346 Icon string `json:"icon"`
347}
348
349func newCueApp(config *cue.Value) (cueApp, error) {
350 if config == nil {
351 return cueApp{}, fmt.Errorf("config not provided")
352 }
353 var cfg cueAppConfig
354 if err := config.Decode(&cfg); err != nil {
355 return cueApp{}, err
356 }
357 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
358 if err != nil {
359 return cueApp{}, err
360 }
361 return cueApp{
362 name: cfg.Name,
363 description: cfg.Description,
364 icon: template.HTML(cfg.Icon),
365 namespace: cfg.Namespace,
366 schema: schema,
367 cfg: config,
368 }, nil
369}
370
371func (a cueApp) Name() string {
372 return a.name
373}
374
375func (a cueApp) Description() string {
376 return a.description
377}
378
379func (a cueApp) Icon() template.HTML {
380 return a.icon
381}
382
383func (a cueApp) Schema() Schema {
384 return a.schema
385}
386
387func (a cueApp) Namespaces() []string {
388 return []string{a.namespace}
389}
390
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400391type PortForward struct {
392 Allocator string `json:"allocator"`
393 Protocol string `json:"protocol"`
394 SourcePort int `json:"sourcePort"`
395 TargetService string `json:"targetService"`
396 TargetPort int `json:"targetPort"`
397}
398
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400399func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400400 ret := Rendered{
401 Resources: make(map[string][]byte),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400402 Ports: make([]PortForward, 0),
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400403 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400404 var buf bytes.Buffer
405 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400406 return Rendered{}, err
407 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400408 ctx := a.cfg.Context()
409 d := ctx.CompileBytes(buf.Bytes())
410 res := a.cfg.Unify(d).Eval()
411 if err := res.Err(); err != nil {
412 return Rendered{}, err
413 }
414 if err := res.Validate(); err != nil {
415 return Rendered{}, err
416 }
417 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
418 if err != nil {
419 return Rendered{}, err
420 }
421 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400422 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
423 return Rendered{}, err
424 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400425 output := res.LookupPath(cue.ParsePath("output"))
426 i, err := output.Fields()
427 if err != nil {
428 return Rendered{}, err
429 }
430 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400431 if contents, err := cueyaml.Encode(i.Value()); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400432 return Rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400433 } else {
434 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
435 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400436 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400437 }
438 return ret, nil
439}
440
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400441type AppRepository interface {
442 GetAll() ([]App, error)
443 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400444}
445
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400446type InMemoryAppRepository struct {
447 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400448}
449
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400450func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
451 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400452}
453
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400454func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400455 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400456 if a.Name() == name {
457 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400458 }
459 }
460 return nil, fmt.Errorf("Application not found: %s", name)
461}
giolekva8aa73e82022-07-09 11:34:39 +0400462
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400463func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400464 return r.apps, nil
465}
466
giolekva8aa73e82022-07-09 11:34:39 +0400467func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400468 return append(
469 createApps(infraAppConfigs),
470 CreateStoreApps()...,
471 )
472}
473
474func CreateStoreApps() []App {
475 return createApps(storeAppConfigs)
476}
477
478func createApps(configs []string) []App {
Giorgi Lekveishvili186eae52024-02-15 14:21:41 +0400479 ret := make([]App, 0)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400480 for _, cfgFile := range configs {
481 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
482 if err != nil {
483 panic(err)
484 }
485 if app, err := newCueApp(cfg); err != nil {
486 panic(err)
487 } else {
488 ret = append(ret, app)
489 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400490 }
491 return ret
492}
493
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400494// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
495// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
496// if err != nil {
497// panic(err)
498// }
499// return StoreApp{
500// App{
501// "maddy",
502// []string{"app-maddy"},
503// []*template.Template{
504// tmpls.Lookup("maddy.yaml"),
505// },
506// schema,
507// nil,
508// nil,
509// },
510// `<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>`,
511// "SMPT/IMAP server to communicate via email.",
512// }
513// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400514
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400515type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400516 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400517}
518
519type appVersion struct {
520 Version string `json:"version"`
521 Urls []string `json:"urls"`
522}
523
524type allAppsResp struct {
525 ApiVersion string `json:"apiVersion"`
526 Entries map[string][]appVersion `json:"entries"`
527}
528
529func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
530 resp, err := http.Get(addr)
531 if err != nil {
532 return err
533 }
534 b, err := io.ReadAll(resp.Body)
535 if err != nil {
536 return err
537 }
538 var apps allAppsResp
539 if err := yaml.Unmarshal(b, &apps); err != nil {
540 return err
541 }
542 for name, conf := range apps.Entries {
543 for _, version := range conf {
544 resp, err := http.Get(version.Urls[0])
545 if err != nil {
546 return err
547 }
548 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
549 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
550 return err
551 }
552 sub, err := fs.Chroot(nameVersion)
553 if err != nil {
554 return err
555 }
556 if err := extractApp(resp.Body, sub); err != nil {
557 return err
558 }
559 }
560 }
561 return nil
562}
563
564func extractApp(archive io.Reader, fs billy.Filesystem) error {
565 uncompressed, err := gzip.NewReader(archive)
566 if err != nil {
567 return err
568 }
569 tarReader := tar.NewReader(uncompressed)
570 for true {
571 header, err := tarReader.Next()
572 if err == io.EOF {
573 break
574 }
575 if err != nil {
576 return err
577 }
578 switch header.Typeflag {
579 case tar.TypeDir:
580 if err := fs.MkdirAll(header.Name, 0755); err != nil {
581 return err
582 }
583 case tar.TypeReg:
584 out, err := fs.Create(header.Name)
585 if err != nil {
586 return err
587 }
588 defer out.Close()
589 if _, err := io.Copy(out, tarReader); err != nil {
590 return err
591 }
592 default:
593 return fmt.Errorf("Uknown type: %s", header.Name)
594 }
595 }
596 return nil
597}
598
599type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400600 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400601 fs billy.Filesystem
602}
603
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400604func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400605 all, err := fs.ReadDir(".")
606 if err != nil {
607 return nil, err
608 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400609 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400610 for _, e := range all {
611 if !e.IsDir() {
612 continue
613 }
614 appFS, err := fs.Chroot(e.Name())
615 if err != nil {
616 return nil, err
617 }
618 app, err := loadApp(appFS)
619 if err != nil {
620 log.Printf("Ignoring directory %s: %s", e.Name(), err)
621 continue
622 }
623 apps = append(apps, app)
624 }
625 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400626 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400627 fs,
628 }, nil
629}
630
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400631func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400632 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400633 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400634 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400635 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400636 var contents bytes.Buffer
637 for _, i := range items {
638 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400639 continue
640 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400641 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400642 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400643 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400644 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400645 defer f.Close()
646 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400647 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400648 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400649 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400650 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400651 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400652 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400653 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400654 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400655}
656
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400657func cleanName(s string) string {
658 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400659}
660
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400661func processCueConfig(contents string) (*cue.Value, error) {
662 ctx := cuecontext.New()
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400663 cfg := ctx.CompileString(contents + cueBaseConfig)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400664 if err := cfg.Err(); err != nil {
665 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400666 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400667 if err := cfg.Validate(); err != nil {
668 return nil, err
669 }
670 return &cfg, nil
671}
672
673func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
674 contents, err := fs.ReadFile(f)
675 if err != nil {
676 return nil, err
677 }
678 return processCueConfig(string(contents))
679}
680
681func createApp(fs embed.FS, configFile string) App {
682 cfg, err := readCueConfigFromFile(fs, configFile)
683 if err != nil {
684 panic(err)
685 }
686 if app, err := newCueApp(cfg); err != nil {
687 panic(err)
688 } else {
689 return app
690 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400691}