blob: 381a84f24864c06177047363bc0967ab81a17374 [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",
gio932849f2024-08-02 09:01:48 +040023 "values-tmpl/coder.cue",
gio3cdee592024-04-17 10:15:56 +040024 "values-tmpl/url-shortener.cue",
gio44f621b2024-04-29 09:44:38 +040025 "values-tmpl/matrix.cue",
26 "values-tmpl/vaultwarden.cue",
gio18d5c682024-05-02 10:30:57 +040027 // "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040028 "values-tmpl/gerrit.cue",
29 "values-tmpl/jenkins.cue",
30 "values-tmpl/zot.cue",
gio18d5c682024-05-02 10:30:57 +040031 // "values-tmpl/penpot.cue",
gio44f621b2024-04-29 09:44:38 +040032 "values-tmpl/soft-serve.cue",
33 "values-tmpl/pihole.cue",
34 // "values-tmpl/maddy.cue",
gio18d5c682024-05-02 10:30:57 +040035 // "values-tmpl/qbittorrent.cue",
36 // "values-tmpl/jellyfin.cue",
gio44f621b2024-04-29 09:44:38 +040037 "values-tmpl/rpuppy.cue",
giocb34ad22024-07-11 08:01:13 +040038 "values-tmpl/certificate-issuer-custom.cue",
gio44f621b2024-04-29 09:44:38 +040039}
40
41var envAppConfigs = []string{
gio266c04f2024-07-03 14:18:45 +040042 "values-tmpl/dodo-app-instance.cue",
gio94904702024-07-26 16:58:34 +040043 "values-tmpl/dodo-app-instance-status.cue",
gio3cdee592024-04-17 10:15:56 +040044 "values-tmpl/certificate-issuer-private.cue",
45 "values-tmpl/certificate-issuer-public.cue",
46 "values-tmpl/appmanager.cue",
47 "values-tmpl/core-auth.cue",
48 "values-tmpl/headscale-user.cue",
49 "values-tmpl/metallb-ipaddresspool.cue",
50 "values-tmpl/private-network.cue",
51 "values-tmpl/welcome.cue",
52 "values-tmpl/memberships.cue",
53 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040054 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040055 "values-tmpl/env-dns.cue",
gio09a3e5b2024-04-26 14:11:06 +040056 "values-tmpl/launcher.cue",
gio3cdee592024-04-17 10:15:56 +040057}
58
59var infraAppConfigs = []string{
60 "values-tmpl/cert-manager.cue",
61 "values-tmpl/config-repo.cue",
62 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040063 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040064 "values-tmpl/env-manager.cue",
65 "values-tmpl/fluxcd-reconciler.cue",
66 "values-tmpl/headscale-controller.cue",
67 "values-tmpl/ingress-public.cue",
68 "values-tmpl/resource-renderer-controller.cue",
69 "values-tmpl/hydra-maester.cue",
70}
71
72type AppRepository interface {
73 GetAll() ([]App, error)
74 Find(name string) (App, error)
Davit Tabidze780a0d02024-08-05 20:53:26 +040075 Filter(query string) ([]App, error)
gio3cdee592024-04-17 10:15:56 +040076}
77
78type InMemoryAppRepository struct {
79 apps []App
80}
81
82func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
83 return InMemoryAppRepository{apps}
84}
85
86func (r InMemoryAppRepository) Find(name string) (App, error) {
87 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040088 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040089 return a, nil
90 }
91 }
92 return nil, fmt.Errorf("Application not found: %s", name)
93}
94
95func (r InMemoryAppRepository) GetAll() ([]App, error) {
96 return r.apps, nil
97}
98
99func CreateAllApps() []App {
100 return append(
101 createInfraApps(),
gio44f621b2024-04-29 09:44:38 +0400102 append(
103 CreateEnvApps(storeEnvAppConfigs),
104 CreateEnvApps(envAppConfigs)...,
105 )...,
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
126func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400127 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400128 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400129 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400130 if err != nil {
131 panic(err)
132 }
gio308105e2024-04-19 13:12:13 +0400133 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400134 "base.cue": []byte(cueBaseConfig),
135 "global.cue": []byte(cueEnvAppGlobal),
136 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400137 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400138 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400139 panic(err)
140 } else {
141 ret = append(ret, app)
142 }
143 }
144 return ret
145}
146
147func createInfraApps() []App {
148 ret := make([]App, 0)
149 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400150 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400151 if err != nil {
152 panic(err)
153 }
gio308105e2024-04-19 13:12:13 +0400154 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400155 "base.cue": []byte(cueBaseConfig),
156 "global.cue": []byte(cueInfraAppGlobal),
157 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400158 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400159 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400160 panic(err)
161 } else {
162 ret = append(ret, app)
163 }
164 }
165 return ret
166}
167
168type httpAppRepository struct {
169 apps []App
170}
171
172type appVersion struct {
173 Version string `json:"version"`
174 Urls []string `json:"urls"`
175}
176
177type allAppsResp struct {
178 ApiVersion string `json:"apiVersion"`
179 Entries map[string][]appVersion `json:"entries"`
180}
181
182func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
183 resp, err := http.Get(addr)
184 if err != nil {
185 return err
186 }
187 b, err := io.ReadAll(resp.Body)
188 if err != nil {
189 return err
190 }
191 var apps allAppsResp
192 if err := yaml.Unmarshal(b, &apps); err != nil {
193 return err
194 }
195 for name, conf := range apps.Entries {
196 for _, version := range conf {
197 resp, err := http.Get(version.Urls[0])
198 if err != nil {
199 return err
200 }
201 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
202 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
203 return err
204 }
205 sub, err := fs.Chroot(nameVersion)
206 if err != nil {
207 return err
208 }
209 if err := extractApp(resp.Body, sub); err != nil {
210 return err
211 }
212 }
213 }
214 return nil
215}
216
217func extractApp(archive io.Reader, fs billy.Filesystem) error {
218 uncompressed, err := gzip.NewReader(archive)
219 if err != nil {
220 return err
221 }
222 tarReader := tar.NewReader(uncompressed)
223 for true {
224 header, err := tarReader.Next()
225 if err == io.EOF {
226 break
227 }
228 if err != nil {
229 return err
230 }
231 switch header.Typeflag {
232 case tar.TypeDir:
233 if err := fs.MkdirAll(header.Name, 0755); err != nil {
234 return err
235 }
236 case tar.TypeReg:
237 out, err := fs.Create(header.Name)
238 if err != nil {
239 return err
240 }
241 defer out.Close()
242 if _, err := io.Copy(out, tarReader); err != nil {
243 return err
244 }
245 default:
246 return fmt.Errorf("Uknown type: %s", header.Name)
247 }
248 }
249 return nil
250}
251
252type fsAppRepository struct {
253 InMemoryAppRepository
254 fs billy.Filesystem
255}
256
257func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
258 all, err := fs.ReadDir(".")
259 if err != nil {
260 return nil, err
261 }
262 apps := make([]App, 0)
263 for _, e := range all {
264 if !e.IsDir() {
265 continue
266 }
267 appFS, err := fs.Chroot(e.Name())
268 if err != nil {
269 return nil, err
270 }
271 app, err := loadApp(appFS)
272 if err != nil {
273 log.Printf("Ignoring directory %s: %s", e.Name(), err)
274 continue
275 }
276 apps = append(apps, app)
277 }
278 return &fsAppRepository{
279 NewInMemoryAppRepository(apps),
280 fs,
281 }, nil
282}
283
284func loadApp(fs billy.Filesystem) (App, error) {
285 items, err := fs.ReadDir(".")
286 if err != nil {
287 return nil, err
288 }
289 var contents bytes.Buffer
290 for _, i := range items {
291 if i.IsDir() {
292 continue
293 }
294 f, err := fs.Open(i.Name())
295 if err != nil {
296 return nil, err
297 }
298 defer f.Close()
299 if _, err := io.Copy(&contents, f); err != nil {
300 return nil, err
301 }
302 }
gio308105e2024-04-19 13:12:13 +0400303 return NewCueEnvApp(CueAppData{
304 "base.cue": []byte(cueBaseConfig),
305 "app.cue": contents.Bytes(),
306 })
gio3cdee592024-04-17 10:15:56 +0400307}
308
gio308105e2024-04-19 13:12:13 +0400309// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
310// contents, err := fs.ReadFile(f)
311// if err != nil {
312// return nil, err
313// }
314// return processCueConfig(string(contents))
315// }
gio3cdee592024-04-17 10:15:56 +0400316
gio308105e2024-04-19 13:12:13 +0400317// func processCueConfig(contents string) (*cue.Value, error) {
318// ctx := cuecontext.New()
319// cfg := ctx.CompileString(contents + cueBaseConfig)
320// if err := cfg.Err(); err != nil {
321// return nil, err
322// }
323// if err := cfg.Validate(); err != nil {
324// return nil, err
325// }
326// return &cfg, nil
327// }
gio3cdee592024-04-17 10:15:56 +0400328
329// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
330// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
331// if err != nil {
332// panic(err)
333// }
334// return StoreApp{
335// App{
336// "maddy",
337// []string{"app-maddy"},
338// []*template.Template{
339// tmpls.Lookup("maddy.yaml"),
340// },
341// schema,
342// nil,
343// nil,
344// },
345// `<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>`,
346// "SMPT/IMAP server to communicate via email.",
347// }
348// }
349
350func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
351 app, err := r.Find(name)
352 if err != nil {
353 return nil, err
354 }
355 if a, ok := app.(EnvApp); ok {
356 return a, nil
357 } else {
358 return nil, fmt.Errorf("not found")
359 }
360}
361
362func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
363 app, err := r.Find(name)
364 if err != nil {
365 return nil, err
366 }
367 if a, ok := app.(InfraApp); ok {
368 return a, nil
369 } else {
370 return nil, fmt.Errorf("not found")
371 }
372}