blob: 509c7e92252c181231b26ede305aa80ddcaa125b [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",
gio404e2372025-07-11 12:50:26 +040027 "values-tmpl/immich.cue",
gio44f621b2024-04-29 09:44:38 +040028 "values-tmpl/vaultwarden.cue",
gio46998892024-10-24 21:07:54 +040029 "values-tmpl/etherpad.cue",
gio5449e272025-07-23 13:14:01 +040030 "values-tmpl/excalidraw.cue",
gio18d5c682024-05-02 10:30:57 +040031 // "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040032 "values-tmpl/gerrit.cue",
33 "values-tmpl/jenkins.cue",
34 "values-tmpl/zot.cue",
gio18d5c682024-05-02 10:30:57 +040035 // "values-tmpl/penpot.cue",
gio44f621b2024-04-29 09:44:38 +040036 "values-tmpl/soft-serve.cue",
37 "values-tmpl/pihole.cue",
38 // "values-tmpl/maddy.cue",
gio18d5c682024-05-02 10:30:57 +040039 // "values-tmpl/qbittorrent.cue",
40 // "values-tmpl/jellyfin.cue",
gio44f621b2024-04-29 09:44:38 +040041 "values-tmpl/rpuppy.cue",
giocb34ad22024-07-11 08:01:13 +040042 "values-tmpl/certificate-issuer-custom.cue",
gio44f621b2024-04-29 09:44:38 +040043}
44
45var envAppConfigs = []string{
gio266c04f2024-07-03 14:18:45 +040046 "values-tmpl/dodo-app-instance.cue",
gio94904702024-07-26 16:58:34 +040047 "values-tmpl/dodo-app-instance-status.cue",
gio3cdee592024-04-17 10:15:56 +040048 "values-tmpl/certificate-issuer-private.cue",
49 "values-tmpl/certificate-issuer-public.cue",
50 "values-tmpl/appmanager.cue",
51 "values-tmpl/core-auth.cue",
gio3cdee592024-04-17 10:15:56 +040052 "values-tmpl/metallb-ipaddresspool.cue",
53 "values-tmpl/private-network.cue",
54 "values-tmpl/welcome.cue",
55 "values-tmpl/memberships.cue",
56 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040057 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040058 "values-tmpl/env-dns.cue",
gio09a3e5b2024-04-26 14:11:06 +040059 "values-tmpl/launcher.cue",
giof6ad2982024-08-23 17:42:49 +040060 "values-tmpl/cluster-network.cue",
gio8f290322024-09-21 15:37:45 +040061 "values-tmpl/longhorn.cue",
gio3cdee592024-04-17 10:15:56 +040062}
63
64var infraAppConfigs = []string{
65 "values-tmpl/cert-manager.cue",
66 "values-tmpl/config-repo.cue",
67 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040068 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040069 "values-tmpl/env-manager.cue",
70 "values-tmpl/fluxcd-reconciler.cue",
71 "values-tmpl/headscale-controller.cue",
72 "values-tmpl/ingress-public.cue",
73 "values-tmpl/resource-renderer-controller.cue",
74 "values-tmpl/hydra-maester.cue",
75}
76
77type AppRepository interface {
78 GetAll() ([]App, error)
79 Find(name string) (App, error)
Davit Tabidze780a0d02024-08-05 20:53:26 +040080 Filter(query string) ([]App, error)
gio3cdee592024-04-17 10:15:56 +040081}
82
83type InMemoryAppRepository struct {
84 apps []App
85}
86
87func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
88 return InMemoryAppRepository{apps}
89}
90
91func (r InMemoryAppRepository) Find(name string) (App, error) {
92 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040093 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040094 return a, nil
95 }
96 }
97 return nil, fmt.Errorf("Application not found: %s", name)
98}
99
100func (r InMemoryAppRepository) GetAll() ([]App, error) {
101 return r.apps, nil
102}
103
104func CreateAllApps() []App {
105 return append(
106 createInfraApps(),
giof6ad2982024-08-23 17:42:49 +0400107 CreateAllEnvApps()...,
gio3cdee592024-04-17 10:15:56 +0400108 )
109}
110
Davit Tabidze780a0d02024-08-05 20:53:26 +0400111func (r InMemoryAppRepository) Filter(query string) ([]App, error) {
112 var filteredApps []App
113 if query == "" {
114 return r.GetAll()
115 }
116 for _, a := range r.apps {
117 if strings.Contains(strings.ToLower(a.Name()), strings.ToLower(query)) {
118 filteredApps = append(filteredApps, a)
119 }
120 }
121 return filteredApps, nil
122}
123
gio3cdee592024-04-17 10:15:56 +0400124func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400125 return CreateEnvApps(storeEnvAppConfigs)
126}
127
giof6ad2982024-08-23 17:42:49 +0400128func CreateAllEnvApps() []App {
129 return append(
130 CreateStoreApps(),
131 CreateEnvApps(envAppConfigs)...,
132 )
133}
134
gio44f621b2024-04-29 09:44:38 +0400135func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400136 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400137 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400138 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400139 if err != nil {
140 panic(err)
141 }
gio308105e2024-04-19 13:12:13 +0400142 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400143 "base.cue": []byte(cueBaseConfig),
144 "global.cue": []byte(cueEnvAppGlobal),
145 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400146 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400147 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400148 panic(err)
149 } else {
150 ret = append(ret, app)
151 }
152 }
153 return ret
154}
155
156func createInfraApps() []App {
157 ret := make([]App, 0)
158 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400159 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400160 if err != nil {
161 panic(err)
162 }
gio308105e2024-04-19 13:12:13 +0400163 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400164 "base.cue": []byte(cueBaseConfig),
165 "global.cue": []byte(cueInfraAppGlobal),
166 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400167 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400168 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400169 panic(err)
170 } else {
171 ret = append(ret, app)
172 }
173 }
174 return ret
175}
176
177type httpAppRepository struct {
178 apps []App
179}
180
181type appVersion struct {
182 Version string `json:"version"`
183 Urls []string `json:"urls"`
184}
185
186type allAppsResp struct {
187 ApiVersion string `json:"apiVersion"`
188 Entries map[string][]appVersion `json:"entries"`
189}
190
191func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
192 resp, err := http.Get(addr)
193 if err != nil {
194 return err
195 }
196 b, err := io.ReadAll(resp.Body)
197 if err != nil {
198 return err
199 }
200 var apps allAppsResp
201 if err := yaml.Unmarshal(b, &apps); err != nil {
202 return err
203 }
204 for name, conf := range apps.Entries {
205 for _, version := range conf {
206 resp, err := http.Get(version.Urls[0])
207 if err != nil {
208 return err
209 }
210 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
211 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
212 return err
213 }
214 sub, err := fs.Chroot(nameVersion)
215 if err != nil {
216 return err
217 }
218 if err := extractApp(resp.Body, sub); err != nil {
219 return err
220 }
221 }
222 }
223 return nil
224}
225
226func extractApp(archive io.Reader, fs billy.Filesystem) error {
227 uncompressed, err := gzip.NewReader(archive)
228 if err != nil {
229 return err
230 }
231 tarReader := tar.NewReader(uncompressed)
232 for true {
233 header, err := tarReader.Next()
234 if err == io.EOF {
235 break
236 }
237 if err != nil {
238 return err
239 }
240 switch header.Typeflag {
241 case tar.TypeDir:
242 if err := fs.MkdirAll(header.Name, 0755); err != nil {
243 return err
244 }
245 case tar.TypeReg:
246 out, err := fs.Create(header.Name)
247 if err != nil {
248 return err
249 }
250 defer out.Close()
251 if _, err := io.Copy(out, tarReader); err != nil {
252 return err
253 }
254 default:
255 return fmt.Errorf("Uknown type: %s", header.Name)
256 }
257 }
258 return nil
259}
260
261type fsAppRepository struct {
262 InMemoryAppRepository
263 fs billy.Filesystem
264}
265
266func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
267 all, err := fs.ReadDir(".")
268 if err != nil {
269 return nil, err
270 }
271 apps := make([]App, 0)
272 for _, e := range all {
273 if !e.IsDir() {
274 continue
275 }
276 appFS, err := fs.Chroot(e.Name())
277 if err != nil {
278 return nil, err
279 }
280 app, err := loadApp(appFS)
281 if err != nil {
282 log.Printf("Ignoring directory %s: %s", e.Name(), err)
283 continue
284 }
285 apps = append(apps, app)
286 }
287 return &fsAppRepository{
288 NewInMemoryAppRepository(apps),
289 fs,
290 }, nil
291}
292
293func loadApp(fs billy.Filesystem) (App, error) {
294 items, err := fs.ReadDir(".")
295 if err != nil {
296 return nil, err
297 }
298 var contents bytes.Buffer
299 for _, i := range items {
300 if i.IsDir() {
301 continue
302 }
303 f, err := fs.Open(i.Name())
304 if err != nil {
305 return nil, err
306 }
307 defer f.Close()
308 if _, err := io.Copy(&contents, f); err != nil {
309 return nil, err
310 }
311 }
gio308105e2024-04-19 13:12:13 +0400312 return NewCueEnvApp(CueAppData{
313 "base.cue": []byte(cueBaseConfig),
314 "app.cue": contents.Bytes(),
315 })
gio3cdee592024-04-17 10:15:56 +0400316}
317
gio308105e2024-04-19 13:12:13 +0400318// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
319// contents, err := fs.ReadFile(f)
320// if err != nil {
321// return nil, err
322// }
323// return processCueConfig(string(contents))
324// }
gio3cdee592024-04-17 10:15:56 +0400325
gio308105e2024-04-19 13:12:13 +0400326// func processCueConfig(contents string) (*cue.Value, error) {
327// ctx := cuecontext.New()
328// cfg := ctx.CompileString(contents + cueBaseConfig)
329// if err := cfg.Err(); err != nil {
330// return nil, err
331// }
332// if err := cfg.Validate(); err != nil {
333// return nil, err
334// }
335// return &cfg, nil
336// }
gio3cdee592024-04-17 10:15:56 +0400337
338// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
339// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
340// if err != nil {
341// panic(err)
342// }
343// return StoreApp{
344// App{
345// "maddy",
346// []string{"app-maddy"},
347// []*template.Template{
348// tmpls.Lookup("maddy.yaml"),
349// },
350// schema,
351// nil,
352// nil,
353// },
354// `<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>`,
355// "SMPT/IMAP server to communicate via email.",
356// }
357// }
358
359func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
360 app, err := r.Find(name)
361 if err != nil {
362 return nil, err
363 }
364 if a, ok := app.(EnvApp); ok {
365 return a, nil
366 } else {
367 return nil, fmt.Errorf("not found")
368 }
369}
370
371func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
372 app, err := r.Find(name)
373 if err != nil {
374 return nil, err
375 }
376 if a, ok := app.(InfraApp); ok {
377 return a, nil
378 } else {
379 return nil, fmt.Errorf("not found")
380 }
381}