blob: 4ce7c9d511fd875aa17bb1dccf88f09fd48189dc [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",
49 "values-tmpl/headscale-user.cue",
50 "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",
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(),
gio44f621b2024-04-29 09:44:38 +0400103 append(
104 CreateEnvApps(storeEnvAppConfigs),
105 CreateEnvApps(envAppConfigs)...,
106 )...,
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
127func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400128 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400129 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400130 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400131 if err != nil {
132 panic(err)
133 }
gio308105e2024-04-19 13:12:13 +0400134 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400135 "base.cue": []byte(cueBaseConfig),
136 "global.cue": []byte(cueEnvAppGlobal),
137 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400138 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400139 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400140 panic(err)
141 } else {
142 ret = append(ret, app)
143 }
144 }
145 return ret
146}
147
148func createInfraApps() []App {
149 ret := make([]App, 0)
150 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400151 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400152 if err != nil {
153 panic(err)
154 }
gio308105e2024-04-19 13:12:13 +0400155 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400156 "base.cue": []byte(cueBaseConfig),
157 "global.cue": []byte(cueInfraAppGlobal),
158 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400159 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400160 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400161 panic(err)
162 } else {
163 ret = append(ret, app)
164 }
165 }
166 return ret
167}
168
169type httpAppRepository struct {
170 apps []App
171}
172
173type appVersion struct {
174 Version string `json:"version"`
175 Urls []string `json:"urls"`
176}
177
178type allAppsResp struct {
179 ApiVersion string `json:"apiVersion"`
180 Entries map[string][]appVersion `json:"entries"`
181}
182
183func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
184 resp, err := http.Get(addr)
185 if err != nil {
186 return err
187 }
188 b, err := io.ReadAll(resp.Body)
189 if err != nil {
190 return err
191 }
192 var apps allAppsResp
193 if err := yaml.Unmarshal(b, &apps); err != nil {
194 return err
195 }
196 for name, conf := range apps.Entries {
197 for _, version := range conf {
198 resp, err := http.Get(version.Urls[0])
199 if err != nil {
200 return err
201 }
202 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
203 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
204 return err
205 }
206 sub, err := fs.Chroot(nameVersion)
207 if err != nil {
208 return err
209 }
210 if err := extractApp(resp.Body, sub); err != nil {
211 return err
212 }
213 }
214 }
215 return nil
216}
217
218func extractApp(archive io.Reader, fs billy.Filesystem) error {
219 uncompressed, err := gzip.NewReader(archive)
220 if err != nil {
221 return err
222 }
223 tarReader := tar.NewReader(uncompressed)
224 for true {
225 header, err := tarReader.Next()
226 if err == io.EOF {
227 break
228 }
229 if err != nil {
230 return err
231 }
232 switch header.Typeflag {
233 case tar.TypeDir:
234 if err := fs.MkdirAll(header.Name, 0755); err != nil {
235 return err
236 }
237 case tar.TypeReg:
238 out, err := fs.Create(header.Name)
239 if err != nil {
240 return err
241 }
242 defer out.Close()
243 if _, err := io.Copy(out, tarReader); err != nil {
244 return err
245 }
246 default:
247 return fmt.Errorf("Uknown type: %s", header.Name)
248 }
249 }
250 return nil
251}
252
253type fsAppRepository struct {
254 InMemoryAppRepository
255 fs billy.Filesystem
256}
257
258func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
259 all, err := fs.ReadDir(".")
260 if err != nil {
261 return nil, err
262 }
263 apps := make([]App, 0)
264 for _, e := range all {
265 if !e.IsDir() {
266 continue
267 }
268 appFS, err := fs.Chroot(e.Name())
269 if err != nil {
270 return nil, err
271 }
272 app, err := loadApp(appFS)
273 if err != nil {
274 log.Printf("Ignoring directory %s: %s", e.Name(), err)
275 continue
276 }
277 apps = append(apps, app)
278 }
279 return &fsAppRepository{
280 NewInMemoryAppRepository(apps),
281 fs,
282 }, nil
283}
284
285func loadApp(fs billy.Filesystem) (App, error) {
286 items, err := fs.ReadDir(".")
287 if err != nil {
288 return nil, err
289 }
290 var contents bytes.Buffer
291 for _, i := range items {
292 if i.IsDir() {
293 continue
294 }
295 f, err := fs.Open(i.Name())
296 if err != nil {
297 return nil, err
298 }
299 defer f.Close()
300 if _, err := io.Copy(&contents, f); err != nil {
301 return nil, err
302 }
303 }
gio308105e2024-04-19 13:12:13 +0400304 return NewCueEnvApp(CueAppData{
305 "base.cue": []byte(cueBaseConfig),
306 "app.cue": contents.Bytes(),
307 })
gio3cdee592024-04-17 10:15:56 +0400308}
309
gio308105e2024-04-19 13:12:13 +0400310// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
311// contents, err := fs.ReadFile(f)
312// if err != nil {
313// return nil, err
314// }
315// return processCueConfig(string(contents))
316// }
gio3cdee592024-04-17 10:15:56 +0400317
gio308105e2024-04-19 13:12:13 +0400318// func processCueConfig(contents string) (*cue.Value, error) {
319// ctx := cuecontext.New()
320// cfg := ctx.CompileString(contents + cueBaseConfig)
321// if err := cfg.Err(); err != nil {
322// return nil, err
323// }
324// if err := cfg.Validate(); err != nil {
325// return nil, err
326// }
327// return &cfg, nil
328// }
gio3cdee592024-04-17 10:15:56 +0400329
330// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
331// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
332// if err != nil {
333// panic(err)
334// }
335// return StoreApp{
336// App{
337// "maddy",
338// []string{"app-maddy"},
339// []*template.Template{
340// tmpls.Lookup("maddy.yaml"),
341// },
342// schema,
343// nil,
344// nil,
345// },
346// `<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>`,
347// "SMPT/IMAP server to communicate via email.",
348// }
349// }
350
351func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
352 app, err := r.Find(name)
353 if err != nil {
354 return nil, err
355 }
356 if a, ok := app.(EnvApp); ok {
357 return a, nil
358 } else {
359 return nil, fmt.Errorf("not found")
360 }
361}
362
363func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
364 app, err := r.Find(name)
365 if err != nil {
366 return nil, err
367 }
368 if a, ok := app.(InfraApp); ok {
369 return a, nil
370 } else {
371 return nil, fmt.Errorf("not found")
372 }
373}