blob: 35c6d11f10e18a5b6c4a76cf710db6f23e0d9baa [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",
gio4a9d83d2024-04-14 13:14:40 +040039 "values-tmpl/zot.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040040}
41
42var infraAppConfigs = []string{
43 "values-tmpl/appmanager.cue",
44 "values-tmpl/cert-manager.cue",
45 "values-tmpl/certificate-issuer-private.cue",
46 "values-tmpl/certificate-issuer-public.cue",
47 "values-tmpl/config-repo.cue",
48 "values-tmpl/core-auth.cue",
49 "values-tmpl/csi-driver-smb.cue",
50 "values-tmpl/dns-zone-manager.cue",
51 "values-tmpl/env-manager.cue",
52 "values-tmpl/fluxcd-reconciler.cue",
53 "values-tmpl/headscale-controller.cue",
54 "values-tmpl/headscale-user.cue",
55 "values-tmpl/headscale.cue",
56 "values-tmpl/ingress-public.cue",
57 "values-tmpl/metallb-ipaddresspool.cue",
58 "values-tmpl/private-network.cue",
59 "values-tmpl/resource-renderer-controller.cue",
60 "values-tmpl/welcome.cue",
DTabidze0d802592024-03-19 17:42:45 +040061 "values-tmpl/memberships.cue",
Giorgi Lekveishvili925f0de2024-03-14 18:51:56 +040062 "values-tmpl/hydra-maester.cue",
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +040063}
64
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040065// 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
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040073#Auth: {
74 enabled: bool | *false // TODO(gio): enabled by default?
75 groups: string | *"" // TODO(gio): []string
76}
77
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040078#Network: {
79 name: string
80 ingressClass: string
81 certificateIssuer: string | *""
82 domain: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040083 allocatePortAddr: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +040084}
85
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040086networks: {
87 public: #Network & {
88 name: "Public"
89 ingressClass: "\(global.pcloudEnvName)-ingress-public"
90 certificateIssuer: "\(global.id)-public"
91 domain: global.domain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040092 allocatePortAddr: "http://port-allocator.\(global.pcloudEnvName)-ingress-public.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040093 }
94 private: #Network & {
95 name: "Private"
96 ingressClass: "\(global.id)-ingress-private"
97 domain: global.privateDomain
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040098 allocatePortAddr: "http://port-allocator.\(global.id)-ingress-private.svc.cluster.local/api/allocate"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +040099 }
100}
101
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400102#Image: {
103 registry: string | *"docker.io"
104 repository: string
105 name: string
106 tag: string
107 pullPolicy: string | *"IfNotPresent"
108 imageName: "\(repository)/\(name)"
109 fullName: "\(registry)/\(imageName)"
110 fullNameWithTag: "\(fullName):\(tag)"
111}
112
113#Chart: {
114 chart: string
115 sourceRef: #SourceRef
116}
117
118#SourceRef: {
119 kind: "GitRepository" | "HelmRepository"
120 name: string
121 namespace: string // TODO(gio): default global.id
122}
123
124#Global: {
125 id: string | *""
126 pcloudEnvName: string | *""
127 domain: string | *""
128 privateDomain: string | *""
129 namespacePrefix: string | *""
130 ...
131}
132
133#Release: {
134 namespace: string
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400135 repoAddr: string
136 appDir: string
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400137}
138
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400139#PortForward: {
140 allocator: string
141 protocol: "TCP" | "UDP" | *"TCP"
142 sourcePort: int
143 targetService: string
144 targetPort: int
145}
146
147portForward: [...#PortForward] | *[]
148
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400149global: #Global
150release: #Release
151
152_ingressPrivate: "\(global.id)-ingress-private"
153_ingressPublic: "\(global.pcloudEnvName)-ingress-public"
154_issuerPrivate: "\(global.id)-private"
155_issuerPublic: "\(global.id)-public"
156
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400157_IngressWithAuthProxy: {
158 inp: {
159 auth: #Auth
160 network: #Network
161 subdomain: string
162 serviceName: string
163 port: { name: string } | { number: int & > 0 }
164 }
165
166 _domain: "\(inp.subdomain).\(inp.network.domain)"
167 _authProxyHTTPPortName: "http"
168
169 out: {
170 images: {
171 authProxy: #Image & {
172 repository: "giolekva"
173 name: "auth-proxy"
174 tag: "latest"
175 pullPolicy: "Always"
176 }
177 }
178 charts: {
179 ingress: #Chart & {
180 chart: "charts/ingress"
181 sourceRef: {
182 kind: "GitRepository"
183 name: "pcloud"
184 namespace: global.id
185 }
186 }
187 authProxy: #Chart & {
188 chart: "charts/auth-proxy"
189 sourceRef: {
190 kind: "GitRepository"
191 name: "pcloud"
192 namespace: global.id
193 }
194 }
195 }
196 helm: {
197 if inp.auth.enabled {
198 "auth-proxy": {
199 chart: charts.authProxy
200 values: {
201 image: {
202 repository: images.authProxy.fullName
203 tag: images.authProxy.tag
204 pullPolicy: images.authProxy.pullPolicy
205 }
206 upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
207 whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
208 loginAddr: "https://accounts-ui.\(global.domain)/login"
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400209 membershipAddr: "http://memberships-api.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400210 groups: inp.auth.groups
211 portName: _authProxyHTTPPortName
212 }
213 }
214 }
215 ingress: {
216 chart: charts.ingress
217 values: {
218 domain: _domain
219 ingressClassName: inp.network.ingressClass
220 certificateIssuer: inp.network.certificateIssuer
221 service: {
222 if inp.auth.enabled {
223 name: "auth-proxy"
224 port: name: _authProxyHTTPPortName
225 }
226 if !inp.auth.enabled {
227 name: inp.serviceName
228 if inp.port.name != _|_ {
229 port: name: inp.port.name
230 }
231 if inp.port.number != _|_ {
232 port: number: inp.port.number
233 }
234 }
235 }
236 }
237 }
238 }
239 }
240}
241
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400242images: {
243 for key, value in images {
244 "\(key)": #Image & value
245 }
246}
247
248charts: {
249 for key, value in charts {
250 "\(key)": #Chart & value
251 }
252}
253
254#ResourceReference: {
255 name: string
256 namespace: string
257}
258
259#Helm: {
260 name: string
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400261 dependsOn: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400262 ...
263}
264
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400265helmValidate: {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400266 for key, value in helm {
267 "\(key)": #Helm & value & {
268 name: key
269 }
270 }
271}
272
273#HelmRelease: {
274 _name: string
275 _chart: #Chart
276 _values: _
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400277 _dependencies: [...#ResourceReference] | *[]
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400278
279 apiVersion: "helm.toolkit.fluxcd.io/v2beta1"
280 kind: "HelmRelease"
281 metadata: {
282 name: _name
283 namespace: release.namespace
284 }
285 spec: {
286 interval: "1m0s"
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400287 dependsOn: _dependencies
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400288 chart: {
289 spec: _chart
290 }
291 values: _values
292 }
293}
294
295output: {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400296 for name, r in helmValidate {
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400297 "\(name)": #HelmRelease & {
298 _name: name
299 _chart: r.chart
300 _values: r.values
301 _dependencies: r.dependsOn
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400302 }
303 }
304}
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400305
306#SSHKey: {
307 public: string
308 private: string
309}
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400310`
311
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400312type appConfig struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400313 Name string `json:"name"`
314 Version string `json:"version"`
315 Description string `json:"description"`
316 Namespaces []string `json:"namespaces"`
317 Icon template.HTML `json:"icon"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318}
319
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400320type Rendered struct {
321 Readme string
322 Resources map[string][]byte
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400323 Ports []PortForward
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400324}
325
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400326type App interface {
327 Name() string
328 Description() string
329 Icon() template.HTML
330 Schema() Schema
gioef01fbb2024-04-12 16:52:59 +0400331 Namespace() string
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400332 Render(derived Derived) (Rendered, error)
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400333}
334
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400335type cueApp struct {
336 name string
337 description string
338 icon template.HTML
339 namespace string
340 schema Schema
341 cfg *cue.Value
342}
343
344type cueAppConfig struct {
345 Name string `json:"name"`
346 Namespace string `json:"namespace"`
347 Description string `json:"description"`
348 Icon string `json:"icon"`
349}
350
351func newCueApp(config *cue.Value) (cueApp, error) {
352 if config == nil {
353 return cueApp{}, fmt.Errorf("config not provided")
354 }
355 var cfg cueAppConfig
356 if err := config.Decode(&cfg); err != nil {
357 return cueApp{}, err
358 }
359 schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
360 if err != nil {
361 return cueApp{}, err
362 }
363 return cueApp{
364 name: cfg.Name,
365 description: cfg.Description,
366 icon: template.HTML(cfg.Icon),
367 namespace: cfg.Namespace,
368 schema: schema,
369 cfg: config,
370 }, nil
371}
372
373func (a cueApp) Name() string {
374 return a.name
375}
376
377func (a cueApp) Description() string {
378 return a.description
379}
380
381func (a cueApp) Icon() template.HTML {
382 return a.icon
383}
384
385func (a cueApp) Schema() Schema {
386 return a.schema
387}
388
gioef01fbb2024-04-12 16:52:59 +0400389func (a cueApp) Namespace() string {
390 return a.namespace
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400391}
392
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400393type PortForward struct {
394 Allocator string `json:"allocator"`
395 Protocol string `json:"protocol"`
396 SourcePort int `json:"sourcePort"`
397 TargetService string `json:"targetService"`
398 TargetPort int `json:"targetPort"`
399}
400
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400401func (a cueApp) Render(derived Derived) (Rendered, error) {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400402 ret := Rendered{
403 Resources: make(map[string][]byte),
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400404 Ports: make([]PortForward, 0),
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400405 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400406 var buf bytes.Buffer
407 if err := json.NewEncoder(&buf).Encode(derived); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400408 return Rendered{}, err
409 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400410 ctx := a.cfg.Context()
411 d := ctx.CompileBytes(buf.Bytes())
412 res := a.cfg.Unify(d).Eval()
413 if err := res.Err(); err != nil {
414 return Rendered{}, err
415 }
416 if err := res.Validate(); err != nil {
417 return Rendered{}, err
418 }
419 readme, err := res.LookupPath(cue.ParsePath("readme")).String()
420 if err != nil {
421 return Rendered{}, err
422 }
423 ret.Readme = readme
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400424 if err := res.LookupPath(cue.ParsePath("portForward")).Decode(&ret.Ports); err != nil {
425 return Rendered{}, err
426 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400427 output := res.LookupPath(cue.ParsePath("output"))
428 i, err := output.Fields()
429 if err != nil {
430 return Rendered{}, err
431 }
432 for i.Next() {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400433 if contents, err := cueyaml.Encode(i.Value()); err != nil {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400434 return Rendered{}, err
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400435 } else {
436 name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
437 ret.Resources[name] = contents
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400438 }
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400439 }
440 return ret, nil
441}
442
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400443type AppRepository interface {
444 GetAll() ([]App, error)
445 Find(name string) (App, error)
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400446}
447
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400448type InMemoryAppRepository struct {
449 apps []App
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400450}
451
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400452func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
453 return InMemoryAppRepository{apps}
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400454}
455
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400456func (r InMemoryAppRepository) Find(name string) (App, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400457 for _, a := range r.apps {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400458 if a.Name() == name {
459 return a, nil
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +0400460 }
461 }
462 return nil, fmt.Errorf("Application not found: %s", name)
463}
giolekva8aa73e82022-07-09 11:34:39 +0400464
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400465func (r InMemoryAppRepository) GetAll() ([]App, error) {
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400466 return r.apps, nil
467}
468
giolekva8aa73e82022-07-09 11:34:39 +0400469func CreateAllApps() []App {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400470 return append(
471 createApps(infraAppConfigs),
472 CreateStoreApps()...,
473 )
474}
475
476func CreateStoreApps() []App {
477 return createApps(storeAppConfigs)
478}
479
480func createApps(configs []string) []App {
Giorgi Lekveishvili186eae52024-02-15 14:21:41 +0400481 ret := make([]App, 0)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400482 for _, cfgFile := range configs {
483 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
484 if err != nil {
485 panic(err)
486 }
487 if app, err := newCueApp(cfg); err != nil {
488 panic(err)
489 } else {
490 ret = append(ret, app)
491 }
Giorgi Lekveishvili27b2b572023-06-30 10:44:45 +0400492 }
493 return ret
494}
495
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400496// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
497// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
498// if err != nil {
499// panic(err)
500// }
501// return StoreApp{
502// App{
503// "maddy",
504// []string{"app-maddy"},
505// []*template.Template{
506// tmpls.Lookup("maddy.yaml"),
507// },
508// schema,
509// nil,
510// nil,
511// },
512// `<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>`,
513// "SMPT/IMAP server to communicate via email.",
514// }
515// }
Giorgi Lekveishvili2df23db2023-12-14 07:55:22 +0400516
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400517type httpAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400518 apps []App
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400519}
520
521type appVersion struct {
522 Version string `json:"version"`
523 Urls []string `json:"urls"`
524}
525
526type allAppsResp struct {
527 ApiVersion string `json:"apiVersion"`
528 Entries map[string][]appVersion `json:"entries"`
529}
530
531func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
532 resp, err := http.Get(addr)
533 if err != nil {
534 return err
535 }
536 b, err := io.ReadAll(resp.Body)
537 if err != nil {
538 return err
539 }
540 var apps allAppsResp
541 if err := yaml.Unmarshal(b, &apps); err != nil {
542 return err
543 }
544 for name, conf := range apps.Entries {
545 for _, version := range conf {
546 resp, err := http.Get(version.Urls[0])
547 if err != nil {
548 return err
549 }
550 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
551 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
552 return err
553 }
554 sub, err := fs.Chroot(nameVersion)
555 if err != nil {
556 return err
557 }
558 if err := extractApp(resp.Body, sub); err != nil {
559 return err
560 }
561 }
562 }
563 return nil
564}
565
566func extractApp(archive io.Reader, fs billy.Filesystem) error {
567 uncompressed, err := gzip.NewReader(archive)
568 if err != nil {
569 return err
570 }
571 tarReader := tar.NewReader(uncompressed)
572 for true {
573 header, err := tarReader.Next()
574 if err == io.EOF {
575 break
576 }
577 if err != nil {
578 return err
579 }
580 switch header.Typeflag {
581 case tar.TypeDir:
582 if err := fs.MkdirAll(header.Name, 0755); err != nil {
583 return err
584 }
585 case tar.TypeReg:
586 out, err := fs.Create(header.Name)
587 if err != nil {
588 return err
589 }
590 defer out.Close()
591 if _, err := io.Copy(out, tarReader); err != nil {
592 return err
593 }
594 default:
595 return fmt.Errorf("Uknown type: %s", header.Name)
596 }
597 }
598 return nil
599}
600
601type fsAppRepository struct {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400602 InMemoryAppRepository
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400603 fs billy.Filesystem
604}
605
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400606func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400607 all, err := fs.ReadDir(".")
608 if err != nil {
609 return nil, err
610 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400611 apps := make([]App, 0)
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400612 for _, e := range all {
613 if !e.IsDir() {
614 continue
615 }
616 appFS, err := fs.Chroot(e.Name())
617 if err != nil {
618 return nil, err
619 }
620 app, err := loadApp(appFS)
621 if err != nil {
622 log.Printf("Ignoring directory %s: %s", e.Name(), err)
623 continue
624 }
625 apps = append(apps, app)
626 }
627 return &fsAppRepository{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400628 NewInMemoryAppRepository(apps),
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400629 fs,
630 }, nil
631}
632
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400633func loadApp(fs billy.Filesystem) (App, error) {
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400634 items, err := fs.ReadDir(".")
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400635 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400636 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400637 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400638 var contents bytes.Buffer
639 for _, i := range items {
640 if i.IsDir() {
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400641 continue
642 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400643 f, err := fs.Open(i.Name())
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400644 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400645 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400646 }
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400647 defer f.Close()
648 if _, err := io.Copy(&contents, f); err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400649 return nil, err
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400650 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400651 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400652 cfg, err := processCueConfig(contents.String())
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400653 if err != nil {
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400654 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400655 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400656 return newCueApp(cfg)
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400657}
658
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400659func cleanName(s string) string {
660 return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400661}
662
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400663func processCueConfig(contents string) (*cue.Value, error) {
664 ctx := cuecontext.New()
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400665 cfg := ctx.CompileString(contents + cueBaseConfig)
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400666 if err := cfg.Err(); err != nil {
667 return nil, err
Giorgi Lekveishvilief21c132024-01-17 18:57:58 +0400668 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400669 if err := cfg.Validate(); err != nil {
670 return nil, err
671 }
672 return &cfg, nil
673}
674
675func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
676 contents, err := fs.ReadFile(f)
677 if err != nil {
678 return nil, err
679 }
680 return processCueConfig(string(contents))
681}
682
683func createApp(fs embed.FS, configFile string) App {
684 cfg, err := readCueConfigFromFile(fs, configFile)
685 if err != nil {
686 panic(err)
687 }
688 if app, err := newCueApp(cfg); err != nil {
689 panic(err)
690 } else {
691 return app
692 }
Giorgi Lekveishvili743fb432023-11-08 17:19:40 +0400693}