blob: e4bfa4dc4d514e5df4a526de174dbbb15813a677 [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",
gio932849f2024-08-02 09:01:48 +040024 "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",
gio3cdee592024-04-17 10:15:56 +040058}
59
60var infraAppConfigs = []string{
61 "values-tmpl/cert-manager.cue",
62 "values-tmpl/config-repo.cue",
63 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040064 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040065 "values-tmpl/env-manager.cue",
66 "values-tmpl/fluxcd-reconciler.cue",
67 "values-tmpl/headscale-controller.cue",
68 "values-tmpl/ingress-public.cue",
69 "values-tmpl/resource-renderer-controller.cue",
70 "values-tmpl/hydra-maester.cue",
71}
72
73type AppRepository interface {
74 GetAll() ([]App, error)
75 Find(name string) (App, error)
Davit Tabidze780a0d02024-08-05 20:53:26 +040076 Filter(query string) ([]App, error)
gio3cdee592024-04-17 10:15:56 +040077}
78
79type InMemoryAppRepository struct {
80 apps []App
81}
82
83func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
84 return InMemoryAppRepository{apps}
85}
86
87func (r InMemoryAppRepository) Find(name string) (App, error) {
88 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040089 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040090 return a, nil
91 }
92 }
93 return nil, fmt.Errorf("Application not found: %s", name)
94}
95
96func (r InMemoryAppRepository) GetAll() ([]App, error) {
97 return r.apps, nil
98}
99
100func CreateAllApps() []App {
101 return append(
102 createInfraApps(),
giof6ad2982024-08-23 17:42:49 +0400103 CreateAllEnvApps()...,
gio3cdee592024-04-17 10:15:56 +0400104 )
105}
106
Davit Tabidze780a0d02024-08-05 20:53:26 +0400107func (r InMemoryAppRepository) Filter(query string) ([]App, error) {
108 var filteredApps []App
109 if query == "" {
110 return r.GetAll()
111 }
112 for _, a := range r.apps {
113 if strings.Contains(strings.ToLower(a.Name()), strings.ToLower(query)) {
114 filteredApps = append(filteredApps, a)
115 }
116 }
117 return filteredApps, nil
118}
119
gio3cdee592024-04-17 10:15:56 +0400120func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400121 return CreateEnvApps(storeEnvAppConfigs)
122}
123
giof6ad2982024-08-23 17:42:49 +0400124func CreateAllEnvApps() []App {
125 return append(
126 CreateStoreApps(),
127 CreateEnvApps(envAppConfigs)...,
128 )
129}
130
gio44f621b2024-04-29 09:44:38 +0400131func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400132 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400133 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400134 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400135 if err != nil {
136 panic(err)
137 }
gio308105e2024-04-19 13:12:13 +0400138 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400139 "base.cue": []byte(cueBaseConfig),
140 "global.cue": []byte(cueEnvAppGlobal),
141 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400142 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400143 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400144 panic(err)
145 } else {
146 ret = append(ret, app)
147 }
148 }
149 return ret
150}
151
152func createInfraApps() []App {
153 ret := make([]App, 0)
154 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400155 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400156 if err != nil {
157 panic(err)
158 }
gio308105e2024-04-19 13:12:13 +0400159 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400160 "base.cue": []byte(cueBaseConfig),
161 "global.cue": []byte(cueInfraAppGlobal),
162 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400163 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400164 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400165 panic(err)
166 } else {
167 ret = append(ret, app)
168 }
169 }
170 return ret
171}
172
173type httpAppRepository struct {
174 apps []App
175}
176
177type appVersion struct {
178 Version string `json:"version"`
179 Urls []string `json:"urls"`
180}
181
182type allAppsResp struct {
183 ApiVersion string `json:"apiVersion"`
184 Entries map[string][]appVersion `json:"entries"`
185}
186
187func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
188 resp, err := http.Get(addr)
189 if err != nil {
190 return err
191 }
192 b, err := io.ReadAll(resp.Body)
193 if err != nil {
194 return err
195 }
196 var apps allAppsResp
197 if err := yaml.Unmarshal(b, &apps); err != nil {
198 return err
199 }
200 for name, conf := range apps.Entries {
201 for _, version := range conf {
202 resp, err := http.Get(version.Urls[0])
203 if err != nil {
204 return err
205 }
206 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
207 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
208 return err
209 }
210 sub, err := fs.Chroot(nameVersion)
211 if err != nil {
212 return err
213 }
214 if err := extractApp(resp.Body, sub); err != nil {
215 return err
216 }
217 }
218 }
219 return nil
220}
221
222func extractApp(archive io.Reader, fs billy.Filesystem) error {
223 uncompressed, err := gzip.NewReader(archive)
224 if err != nil {
225 return err
226 }
227 tarReader := tar.NewReader(uncompressed)
228 for true {
229 header, err := tarReader.Next()
230 if err == io.EOF {
231 break
232 }
233 if err != nil {
234 return err
235 }
236 switch header.Typeflag {
237 case tar.TypeDir:
238 if err := fs.MkdirAll(header.Name, 0755); err != nil {
239 return err
240 }
241 case tar.TypeReg:
242 out, err := fs.Create(header.Name)
243 if err != nil {
244 return err
245 }
246 defer out.Close()
247 if _, err := io.Copy(out, tarReader); err != nil {
248 return err
249 }
250 default:
251 return fmt.Errorf("Uknown type: %s", header.Name)
252 }
253 }
254 return nil
255}
256
257type fsAppRepository struct {
258 InMemoryAppRepository
259 fs billy.Filesystem
260}
261
262func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
263 all, err := fs.ReadDir(".")
264 if err != nil {
265 return nil, err
266 }
267 apps := make([]App, 0)
268 for _, e := range all {
269 if !e.IsDir() {
270 continue
271 }
272 appFS, err := fs.Chroot(e.Name())
273 if err != nil {
274 return nil, err
275 }
276 app, err := loadApp(appFS)
277 if err != nil {
278 log.Printf("Ignoring directory %s: %s", e.Name(), err)
279 continue
280 }
281 apps = append(apps, app)
282 }
283 return &fsAppRepository{
284 NewInMemoryAppRepository(apps),
285 fs,
286 }, nil
287}
288
289func loadApp(fs billy.Filesystem) (App, error) {
290 items, err := fs.ReadDir(".")
291 if err != nil {
292 return nil, err
293 }
294 var contents bytes.Buffer
295 for _, i := range items {
296 if i.IsDir() {
297 continue
298 }
299 f, err := fs.Open(i.Name())
300 if err != nil {
301 return nil, err
302 }
303 defer f.Close()
304 if _, err := io.Copy(&contents, f); err != nil {
305 return nil, err
306 }
307 }
gio308105e2024-04-19 13:12:13 +0400308 return NewCueEnvApp(CueAppData{
309 "base.cue": []byte(cueBaseConfig),
310 "app.cue": contents.Bytes(),
311 })
gio3cdee592024-04-17 10:15:56 +0400312}
313
gio308105e2024-04-19 13:12:13 +0400314// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
315// contents, err := fs.ReadFile(f)
316// if err != nil {
317// return nil, err
318// }
319// return processCueConfig(string(contents))
320// }
gio3cdee592024-04-17 10:15:56 +0400321
gio308105e2024-04-19 13:12:13 +0400322// func processCueConfig(contents string) (*cue.Value, error) {
323// ctx := cuecontext.New()
324// cfg := ctx.CompileString(contents + cueBaseConfig)
325// if err := cfg.Err(); err != nil {
326// return nil, err
327// }
328// if err := cfg.Validate(); err != nil {
329// return nil, err
330// }
331// return &cfg, nil
332// }
gio3cdee592024-04-17 10:15:56 +0400333
334// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
335// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
336// if err != nil {
337// panic(err)
338// }
339// return StoreApp{
340// App{
341// "maddy",
342// []string{"app-maddy"},
343// []*template.Template{
344// tmpls.Lookup("maddy.yaml"),
345// },
346// schema,
347// nil,
348// nil,
349// },
350// `<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>`,
351// "SMPT/IMAP server to communicate via email.",
352// }
353// }
354
355func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
356 app, err := r.Find(name)
357 if err != nil {
358 return nil, err
359 }
360 if a, ok := app.(EnvApp); ok {
361 return a, nil
362 } else {
363 return nil, fmt.Errorf("not found")
364 }
365}
366
367func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
368 app, err := r.Find(name)
369 if err != nil {
370 return nil, err
371 }
372 if a, ok := app.(InfraApp); ok {
373 return a, nil
374 } else {
375 return nil, fmt.Errorf("not found")
376 }
377}