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