blob: 73a8e642d9360bb44b62dfd4510e0d6598d192e7 [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 Lekveishvili35982662024-04-05 13:05:40 +040038 "values-tmpl/jenkins.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040039}
40
41var infraAppConfigs = []string{
42 "values-tmpl/appmanager.cue",
43 "values-tmpl/cert-manager.cue",
44 "values-tmpl/certificate-issuer-private.cue",
45 "values-tmpl/certificate-issuer-public.cue",
46 "values-tmpl/config-repo.cue",
47 "values-tmpl/core-auth.cue",
48 "values-tmpl/csi-driver-smb.cue",
49 "values-tmpl/dns-zone-manager.cue",
50 "values-tmpl/env-manager.cue",
51 "values-tmpl/fluxcd-reconciler.cue",
52 "values-tmpl/headscale-controller.cue",
53 "values-tmpl/headscale-user.cue",
54 "values-tmpl/headscale.cue",
55 "values-tmpl/ingress-public.cue",
56 "values-tmpl/metallb-ipaddresspool.cue",
57 "values-tmpl/private-network.cue",
58 "values-tmpl/resource-renderer-controller.cue",
59 "values-tmpl/welcome.cue",
DTabidze0d802592024-03-19 17:42:45 +040060 "values-tmpl/memberships.cue",
Giorgi Lekveishvili925f0de2024-03-14 18:51:56 +040061 "values-tmpl/hydra-maester.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040062}
63
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040064// TODO(gio): import
65const cueBaseConfig = `
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040066name: string | *""
67description: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040068readme: string | *""
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040069icon: string | *""
70namespace: string | *""
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040071
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040072#Auth: {
73 enabled: bool | *false // TODO(gio): enabled by default?
74 groups: string | *"" // TODO(gio): []string
75}
76
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040077#Network: {
78 name: string
79 ingressClass: string
80 certificateIssuer: string | *""
81 domain: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040082 allocatePortAddr: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040083}
84
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040085networks: {
86 public: #Network & {
87 name: "Public"
88 ingressClass: "\(global.pcloudEnvName)-ingress-public"
89 certificateIssuer: "\(global.id)-public"
90 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040091 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040092 }
93 private: #Network & {
94 name: "Private"
95 ingressClass: "\(global.id)-ingress-private"
96 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040097 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040098 }
99}
100
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400101#Image: {
102 registry: string | *"docker.io"
103 repository: string
104 name: string
105 tag: string
106 pullPolicy: string | *"IfNotPresent"
107 imageName: "\(repository)/\(name)"
108 fullName: "\(registry)/\(imageName)"
109 fullNameWithTag: "\(fullName):\(tag)"
110}
111
112#Chart: {
113 chart: string
114 sourceRef: #SourceRef
115}
116
117#SourceRef: {
118 kind: "GitRepository" | "HelmRepository"
119 name: string
120 namespace: string // TODO(gio): default global.id
121}
122
123#Global: {
124 id: string | *""
125 pcloudEnvName: string | *""
126 domain: string | *""
127 privateDomain: string | *""
128 namespacePrefix: string | *""
129 ...
130}
131
132#Release: {
133 namespace: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400134 repoAddr: string
135 appDir: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400136}
137
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400138#PortForward: {
139 allocator: string
140 protocol: "TCP" | "UDP" | *"TCP"
141 sourcePort: int
142 targetService: string
143 targetPort: int
144}
145
146portForward: [...#PortForward] | *[]
147
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400148global: #Global
149release: #Release
150
151_ingressPrivate: "\(global.id)-ingress-private"
152_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
153_issuerPrivate: "\(global.id)-private"
154_issuerPublic: "\(global.id)-public"
155
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400156_IngressWithAuthProxy: {
157 inp: {
158 auth: #Auth
159 network: #Network
160 subdomain: string
161 serviceName: string
162 port: { name: string } | { number: int & > 0 }
163 }
164
165 _domain: "\(inp.subdomain).\(inp.network.domain)"
166 _authProxyHTTPPortName: "http"
167
168 out: {
169 images: {
170 authProxy: #Image & {
171 repository: "giolekva"
172 name: "auth-proxy"
173 tag: "latest"
174 pullPolicy: "Always"
175 }
176 }
177 charts: {
178 ingress: #Chart & {
179 chart: "charts/ingress"
180 sourceRef: {
181 kind: "GitRepository"
182 name: "pcloud"
183 namespace: global.id
184 }
185 }
186 authProxy: #Chart & {
187 chart: "charts/auth-proxy"
188 sourceRef: {
189 kind: "GitRepository"
190 name: "pcloud"
191 namespace: global.id
192 }
193 }
194 }
195 helm: {
196 if inp.auth.enabled {
197 "auth-proxy": {
198 chart: charts.authProxy
199 values: {
200 image: {
201 repository: images.authProxy.fullName
202 tag: images.authProxy.tag
203 pullPolicy: images.authProxy.pullPolicy
204 }
205 upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
206 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
207 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400208 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400209 groups: inp.auth.groups
210 portName: _authProxyHTTPPortName
211 }
212 }
213 }
214 ingress: {
215 chart: charts.ingress
216 values: {
217 domain: _domain
218 ingressClassName: inp.network.ingressClass
219 certificateIssuer: inp.network.certificateIssuer
220 service: {
221 if inp.auth.enabled {
222 name: "auth-proxy"
223 port: name: _authProxyHTTPPortName
224 }
225 if !inp.auth.enabled {
226 name: inp.serviceName
227 if inp.port.name != _|_ {
228 port: name: inp.port.name
229 }
230 if inp.port.number != _|_ {
231 port: number: inp.port.number
232 }
233 }
234 }
235 }
236 }
237 }
238 }
239}
240
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400241images: {
242 for key, value in images {
243 "\(key)": #Image & value
244 }
245}
246
247charts: {
248 for key, value in charts {
249 "\(key)": #Chart & value
250 }
251}
252
253#ResourceReference: {
254 name: string
255 namespace: string
256}
257
258#Helm: {
259 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400260 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400261 ...
262}
263
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400264helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400265 for key, value in helm {
266 "\(key)": #Helm & value & {
267 name: key
268 }
269 }
270}
271
272#HelmRelease: {
273 _name: string
274 _chart: #Chart
275 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400276 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400277
278 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
279 kind: "HelmRelease"
280 metadata: {
281 name: _name
282 namespace: release.namespace
283 }
284 spec: {
285 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400286 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400287 chart: {
288 spec: _chart
289 }
290 values: _values
291 }
292}
293
294output: {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400295 for name, r in helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400296 "\(name)": #HelmRelease & {
297 _name: name
298 _chart: r.chart
299 _values: r.values
300 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400301 }
302 }
303}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400304
305#SSHKey: {
306 public: string
307 private: string
308}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400309`
310
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400311type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400312 Name string `json:"name"`
313 Version string `json:"version"`
314 Description string `json:"description"`
315 Namespaces []string `json:"namespaces"`
316 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400317}
318
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400319type Rendered struct {
320 Readme string
321 Resources map[string][]byte
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400322 Ports []PortForward
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400323}
324
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400325type App interface {
326 Name() string
327 Description() string
328 Icon() template.HTML
329 Schema() Schema
330 Namespaces() []string
331 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400332}
333
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400334type cueApp struct {
335 name string
336 description string
337 icon template.HTML
338 namespace string
339 schema Schema
340 cfg *cue.Value
341}
342
343type cueAppConfig struct {
344 Name string `json:"name"`
345 Namespace string `json:"namespace"`
346 Description string `json:"description"`
347 Icon string `json:"icon"`
348}
349
350func newCueApp(config *cue.Value) (cueApp, error) {
351 if config == nil {
352 return cueApp{}, fmt.Errorf("config not provided")
353 }
354 var cfg cueAppConfig
355 if err := config.Decode(&cfg); err != nil {
356 return cueApp{}, err
357 }
358 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
359 if err != nil {
360 return cueApp{}, err
361 }
362 return cueApp{
363 name: cfg.Name,
364 description: cfg.Description,
365 icon: template.HTML(cfg.Icon),
366 namespace: cfg.Namespace,
367 schema: schema,
368 cfg: config,
369 }, nil
370}
371
372func (a cueApp) Name() string {
373 return a.name
374}
375
376func (a cueApp) Description() string {
377 return a.description
378}
379
380func (a cueApp) Icon() template.HTML {
381 return a.icon
382}
383
384func (a cueApp) Schema() Schema {
385 return a.schema
386}
387
388func (a cueApp) Namespaces() []string {
389 return []string{a.namespace}
390}
391
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400392type PortForward struct {
393 Allocator string `json:"allocator"`
394 Protocol string `json:"protocol"`
395 SourcePort int `json:"sourcePort"`
396 TargetService string `json:"targetService"`
397 TargetPort int `json:"targetPort"`
398}
399
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400400func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400401 ret := Rendered{
402 Resources: make(map[string][]byte),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400403 Ports: make([]PortForward, 0),
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400404 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400405 var buf bytes.Buffer
406 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400407 return Rendered{}, err
408 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400409 ctx := a.cfg.Context()
410 d := ctx.CompileBytes(buf.Bytes())
411 res := a.cfg.Unify(d).Eval()
412 if err := res.Err(); err != nil {
413 return Rendered{}, err
414 }
415 if err := res.Validate(); err != nil {
416 return Rendered{}, err
417 }
418 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
419 if err != nil {
420 return Rendered{}, err
421 }
422 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400423 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
424 return Rendered{}, err
425 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400426 output := res.LookupPath(cue.ParsePath("output"))
427 i, err := output.Fields()
428 if err != nil {
429 return Rendered{}, err
430 }
431 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400432 if contents, err := cueyaml.Encode(i.Value()); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400433 return Rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400434 } else {
435 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
436 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400437 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400438 }
439 return ret, nil
440}
441
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400442type AppRepository interface {
443 GetAll() ([]App, error)
444 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400445}
446
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400447type InMemoryAppRepository struct {
448 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400449}
450
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400451func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
452 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400453}
454
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400455func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400456 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400457 if a.Name() == name {
458 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400459 }
460 }
461 return nil, fmt.Errorf("Application not found: %s", name)
462}
giolekva8aa73e82022-07-09 11:34:39 +0400463
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400464func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400465 return r.apps, nil
466}
467
giolekva8aa73e82022-07-09 11:34:39 +0400468func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400469 return append(
470 createApps(infraAppConfigs),
471 CreateStoreApps()...,
472 )
473}
474
475func CreateStoreApps() []App {
476 return createApps(storeAppConfigs)
477}
478
479func createApps(configs []string) []App {
Giorgi Lekveishvili186eae52024-02-15 14:21:41 +0400480 ret := make([]App, 0)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400481 for _, cfgFile := range configs {
482 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
483 if err != nil {
484 panic(err)
485 }
486 if app, err := newCueApp(cfg); err != nil {
487 panic(err)
488 } else {
489 ret = append(ret, app)
490 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400491 }
492 return ret
493}
494
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400495// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
496// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
497// if err != nil {
498// panic(err)
499// }
500// return StoreApp{
501// App{
502// "maddy",
503// []string{"app-maddy"},
504// []*template.Template{
505// tmpls.Lookup("maddy.yaml"),
506// },
507// schema,
508// nil,
509// nil,
510// },
511// `<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>`,
512// "SMPT/IMAP server to communicate via email.",
513// }
514// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400515
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400516type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400517 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400518}
519
520type appVersion struct {
521 Version string `json:"version"`
522 Urls []string `json:"urls"`
523}
524
525type allAppsResp struct {
526 ApiVersion string `json:"apiVersion"`
527 Entries map[string][]appVersion `json:"entries"`
528}
529
530func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
531 resp, err := http.Get(addr)
532 if err != nil {
533 return err
534 }
535 b, err := io.ReadAll(resp.Body)
536 if err != nil {
537 return err
538 }
539 var apps allAppsResp
540 if err := yaml.Unmarshal(b, &apps); err != nil {
541 return err
542 }
543 for name, conf := range apps.Entries {
544 for _, version := range conf {
545 resp, err := http.Get(version.Urls[0])
546 if err != nil {
547 return err
548 }
549 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
550 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
551 return err
552 }
553 sub, err := fs.Chroot(nameVersion)
554 if err != nil {
555 return err
556 }
557 if err := extractApp(resp.Body, sub); err != nil {
558 return err
559 }
560 }
561 }
562 return nil
563}
564
565func extractApp(archive io.Reader, fs billy.Filesystem) error {
566 uncompressed, err := gzip.NewReader(archive)
567 if err != nil {
568 return err
569 }
570 tarReader := tar.NewReader(uncompressed)
571 for true {
572 header, err := tarReader.Next()
573 if err == io.EOF {
574 break
575 }
576 if err != nil {
577 return err
578 }
579 switch header.Typeflag {
580 case tar.TypeDir:
581 if err := fs.MkdirAll(header.Name, 0755); err != nil {
582 return err
583 }
584 case tar.TypeReg:
585 out, err := fs.Create(header.Name)
586 if err != nil {
587 return err
588 }
589 defer out.Close()
590 if _, err := io.Copy(out, tarReader); err != nil {
591 return err
592 }
593 default:
594 return fmt.Errorf("Uknown type: %s", header.Name)
595 }
596 }
597 return nil
598}
599
600type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400601 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400602 fs billy.Filesystem
603}
604
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400605func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400606 all, err := fs.ReadDir(".")
607 if err != nil {
608 return nil, err
609 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400610 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400611 for _, e := range all {
612 if !e.IsDir() {
613 continue
614 }
615 appFS, err := fs.Chroot(e.Name())
616 if err != nil {
617 return nil, err
618 }
619 app, err := loadApp(appFS)
620 if err != nil {
621 log.Printf("Ignoring directory %s: %s", e.Name(), err)
622 continue
623 }
624 apps = append(apps, app)
625 }
626 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400627 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400628 fs,
629 }, nil
630}
631
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400632func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400633 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400634 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400635 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400636 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400637 var contents bytes.Buffer
638 for _, i := range items {
639 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400640 continue
641 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400642 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400643 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400644 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400645 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400646 defer f.Close()
647 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400648 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400649 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400650 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400651 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400652 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400653 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400654 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400655 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400656}
657
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400658func cleanName(s string) string {
659 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400660}
661
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400662func processCueConfig(contents string) (*cue.Value, error) {
663 ctx := cuecontext.New()
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400664 cfg := ctx.CompileString(contents + cueBaseConfig)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400665 if err := cfg.Err(); err != nil {
666 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400667 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400668 if err := cfg.Validate(); err != nil {
669 return nil, err
670 }
671 return &cfg, nil
672}
673
674func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
675 contents, err := fs.ReadFile(f)
676 if err != nil {
677 return nil, err
678 }
679 return processCueConfig(string(contents))
680}
681
682func createApp(fs embed.FS, configFile string) App {
683 cfg, err := readCueConfigFromFile(fs, configFile)
684 if err != nil {
685 panic(err)
686 }
687 if app, err := newCueApp(cfg); err != nil {
688 panic(err)
689 } else {
690 return app
691 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400692}