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