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