blob: e4f462ec7f8cc15dcd3ed31343cf30453196d7f8 [file] [log] [blame]
gio3cdee592024-04-17 10:15:56 +04001package installer
2
3import (
4 "archive/tar"
5 "bytes"
6 "compress/gzip"
7 "embed"
8 "fmt"
9 "io"
10 "log"
11 "net/http"
Davit Tabidze780a0d02024-08-05 20:53:26 +040012 "strings"
gio3cdee592024-04-17 10:15:56 +040013
gio3cdee592024-04-17 10:15:56 +040014 "github.com/go-git/go-billy/v5"
15 "sigs.k8s.io/yaml"
16)
17
18//go:embed values-tmpl
19var valuesTmpls embed.FS
20
gio44f621b2024-04-29 09:44:38 +040021var storeEnvAppConfigs = []string{
gio0eaf2712024-04-14 13:08:46 +040022 "values-tmpl/dodo-app.cue",
gio36b23b32024-08-25 12:20:54 +040023 "values-tmpl/virtual-machine.cue",
gioc81a8472024-09-24 13:06:19 +020024 // "values-tmpl/coder.cue",
gio3cdee592024-04-17 10:15:56 +040025 "values-tmpl/url-shortener.cue",
gio44f621b2024-04-29 09:44:38 +040026 "values-tmpl/matrix.cue",
27 "values-tmpl/vaultwarden.cue",
gio18d5c682024-05-02 10:30:57 +040028 // "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040029 "values-tmpl/gerrit.cue",
30 "values-tmpl/jenkins.cue",
31 "values-tmpl/zot.cue",
gio18d5c682024-05-02 10:30:57 +040032 // "values-tmpl/penpot.cue",
gio44f621b2024-04-29 09:44:38 +040033 "values-tmpl/soft-serve.cue",
34 "values-tmpl/pihole.cue",
35 // "values-tmpl/maddy.cue",
gio18d5c682024-05-02 10:30:57 +040036 // "values-tmpl/qbittorrent.cue",
37 // "values-tmpl/jellyfin.cue",
gio44f621b2024-04-29 09:44:38 +040038 "values-tmpl/rpuppy.cue",
giocb34ad22024-07-11 08:01:13 +040039 "values-tmpl/certificate-issuer-custom.cue",
gio44f621b2024-04-29 09:44:38 +040040}
41
42var envAppConfigs = []string{
gio266c04f2024-07-03 14:18:45 +040043 "values-tmpl/dodo-app-instance.cue",
gio94904702024-07-26 16:58:34 +040044 "values-tmpl/dodo-app-instance-status.cue",
gio3cdee592024-04-17 10:15:56 +040045 "values-tmpl/certificate-issuer-private.cue",
46 "values-tmpl/certificate-issuer-public.cue",
47 "values-tmpl/appmanager.cue",
48 "values-tmpl/core-auth.cue",
gio3cdee592024-04-17 10:15:56 +040049 "values-tmpl/metallb-ipaddresspool.cue",
50 "values-tmpl/private-network.cue",
51 "values-tmpl/welcome.cue",
52 "values-tmpl/memberships.cue",
53 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040054 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040055 "values-tmpl/env-dns.cue",
gio09a3e5b2024-04-26 14:11:06 +040056 "values-tmpl/launcher.cue",
giof6ad2982024-08-23 17:42:49 +040057 "values-tmpl/cluster-network.cue",
gio8f290322024-09-21 15:37:45 +040058 "values-tmpl/longhorn.cue",
gio3cdee592024-04-17 10:15:56 +040059}
60
61var infraAppConfigs = []string{
62 "values-tmpl/cert-manager.cue",
63 "values-tmpl/config-repo.cue",
64 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040065 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040066 "values-tmpl/env-manager.cue",
67 "values-tmpl/fluxcd-reconciler.cue",
68 "values-tmpl/headscale-controller.cue",
69 "values-tmpl/ingress-public.cue",
70 "values-tmpl/resource-renderer-controller.cue",
71 "values-tmpl/hydra-maester.cue",
72}
73
74type AppRepository interface {
75 GetAll() ([]App, error)
76 Find(name string) (App, error)
Davit Tabidze780a0d02024-08-05 20:53:26 +040077 Filter(query string) ([]App, error)
gio3cdee592024-04-17 10:15:56 +040078}
79
80type InMemoryAppRepository struct {
81 apps []App
82}
83
84func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
85 return InMemoryAppRepository{apps}
86}
87
88func (r InMemoryAppRepository) Find(name string) (App, error) {
89 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040090 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040091 return a, nil
92 }
93 }
94 return nil, fmt.Errorf("Application not found: %s", name)
95}
96
97func (r InMemoryAppRepository) GetAll() ([]App, error) {
98 return r.apps, nil
99}
100
101func CreateAllApps() []App {
102 return append(
103 createInfraApps(),
giof6ad2982024-08-23 17:42:49 +0400104 CreateAllEnvApps()...,
gio3cdee592024-04-17 10:15:56 +0400105 )
106}
107
Davit Tabidze780a0d02024-08-05 20:53:26 +0400108func (r InMemoryAppRepository) Filter(query string) ([]App, error) {
109 var filteredApps []App
110 if query == "" {
111 return r.GetAll()
112 }
113 for _, a := range r.apps {
114 if strings.Contains(strings.ToLower(a.Name()), strings.ToLower(query)) {
115 filteredApps = append(filteredApps, a)
116 }
117 }
118 return filteredApps, nil
119}
120
gio3cdee592024-04-17 10:15:56 +0400121func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400122 return CreateEnvApps(storeEnvAppConfigs)
123}
124
giof6ad2982024-08-23 17:42:49 +0400125func CreateAllEnvApps() []App {
126 return append(
127 CreateStoreApps(),
128 CreateEnvApps(envAppConfigs)...,
129 )
130}
131
gio44f621b2024-04-29 09:44:38 +0400132func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400133 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400134 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400135 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400136 if err != nil {
137 panic(err)
138 }
gio308105e2024-04-19 13:12:13 +0400139 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400140 "base.cue": []byte(cueBaseConfig),
141 "global.cue": []byte(cueEnvAppGlobal),
142 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400143 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400144 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400145 panic(err)
146 } else {
147 ret = append(ret, app)
148 }
149 }
150 return ret
151}
152
153func createInfraApps() []App {
154 ret := make([]App, 0)
155 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400156 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400157 if err != nil {
158 panic(err)
159 }
gio308105e2024-04-19 13:12:13 +0400160 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400161 "base.cue": []byte(cueBaseConfig),
162 "global.cue": []byte(cueInfraAppGlobal),
163 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400164 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400165 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400166 panic(err)
167 } else {
168 ret = append(ret, app)
169 }
170 }
171 return ret
172}
173
174type httpAppRepository struct {
175 apps []App
176}
177
178type appVersion struct {
179 Version string `json:"version"`
180 Urls []string `json:"urls"`
181}
182
183type allAppsResp struct {
184 ApiVersion string `json:"apiVersion"`
185 Entries map[string][]appVersion `json:"entries"`
186}
187
188func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
189 resp, err := http.Get(addr)
190 if err != nil {
191 return err
192 }
193 b, err := io.ReadAll(resp.Body)
194 if err != nil {
195 return err
196 }
197 var apps allAppsResp
198 if err := yaml.Unmarshal(b, &apps); err != nil {
199 return err
200 }
201 for name, conf := range apps.Entries {
202 for _, version := range conf {
203 resp, err := http.Get(version.Urls[0])
204 if err != nil {
205 return err
206 }
207 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
208 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
209 return err
210 }
211 sub, err := fs.Chroot(nameVersion)
212 if err != nil {
213 return err
214 }
215 if err := extractApp(resp.Body, sub); err != nil {
216 return err
217 }
218 }
219 }
220 return nil
221}
222
223func extractApp(archive io.Reader, fs billy.Filesystem) error {
224 uncompressed, err := gzip.NewReader(archive)
225 if err != nil {
226 return err
227 }
228 tarReader := tar.NewReader(uncompressed)
229 for true {
230 header, err := tarReader.Next()
231 if err == io.EOF {
232 break
233 }
234 if err != nil {
235 return err
236 }
237 switch header.Typeflag {
238 case tar.TypeDir:
239 if err := fs.MkdirAll(header.Name, 0755); err != nil {
240 return err
241 }
242 case tar.TypeReg:
243 out, err := fs.Create(header.Name)
244 if err != nil {
245 return err
246 }
247 defer out.Close()
248 if _, err := io.Copy(out, tarReader); err != nil {
249 return err
250 }
251 default:
252 return fmt.Errorf("Uknown type: %s", header.Name)
253 }
254 }
255 return nil
256}
257
258type fsAppRepository struct {
259 InMemoryAppRepository
260 fs billy.Filesystem
261}
262
263func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
264 all, err := fs.ReadDir(".")
265 if err != nil {
266 return nil, err
267 }
268 apps := make([]App, 0)
269 for _, e := range all {
270 if !e.IsDir() {
271 continue
272 }
273 appFS, err := fs.Chroot(e.Name())
274 if err != nil {
275 return nil, err
276 }
277 app, err := loadApp(appFS)
278 if err != nil {
279 log.Printf("Ignoring directory %s: %s", e.Name(), err)
280 continue
281 }
282 apps = append(apps, app)
283 }
284 return &fsAppRepository{
285 NewInMemoryAppRepository(apps),
286 fs,
287 }, nil
288}
289
290func loadApp(fs billy.Filesystem) (App, error) {
291 items, err := fs.ReadDir(".")
292 if err != nil {
293 return nil, err
294 }
295 var contents bytes.Buffer
296 for _, i := range items {
297 if i.IsDir() {
298 continue
299 }
300 f, err := fs.Open(i.Name())
301 if err != nil {
302 return nil, err
303 }
304 defer f.Close()
305 if _, err := io.Copy(&contents, f); err != nil {
306 return nil, err
307 }
308 }
gio308105e2024-04-19 13:12:13 +0400309 return NewCueEnvApp(CueAppData{
310 "base.cue": []byte(cueBaseConfig),
311 "app.cue": contents.Bytes(),
312 })
gio3cdee592024-04-17 10:15:56 +0400313}
314
gio308105e2024-04-19 13:12:13 +0400315// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
316// contents, err := fs.ReadFile(f)
317// if err != nil {
318// return nil, err
319// }
320// return processCueConfig(string(contents))
321// }
gio3cdee592024-04-17 10:15:56 +0400322
gio308105e2024-04-19 13:12:13 +0400323// func processCueConfig(contents string) (*cue.Value, error) {
324// ctx := cuecontext.New()
325// cfg := ctx.CompileString(contents + cueBaseConfig)
326// if err := cfg.Err(); err != nil {
327// return nil, err
328// }
329// if err := cfg.Validate(); err != nil {
330// return nil, err
331// }
332// return &cfg, nil
333// }
gio3cdee592024-04-17 10:15:56 +0400334
335// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
336// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
337// if err != nil {
338// panic(err)
339// }
340// return StoreApp{
341// App{
342// "maddy",
343// []string{"app-maddy"},
344// []*template.Template{
345// tmpls.Lookup("maddy.yaml"),
346// },
347// schema,
348// nil,
349// nil,
350// },
351// `<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>`,
352// "SMPT/IMAP server to communicate via email.",
353// }
354// }
355
356func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
357 app, err := r.Find(name)
358 if err != nil {
359 return nil, err
360 }
361 if a, ok := app.(EnvApp); ok {
362 return a, nil
363 } else {
364 return nil, fmt.Errorf("not found")
365 }
366}
367
368func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
369 app, err := r.Find(name)
370 if err != nil {
371 return nil, err
372 }
373 if a, ok := app.(InfraApp); ok {
374 return a, nil
375 } else {
376 return nil, fmt.Errorf("not found")
377 }
378}