blob: b51f766c62af3c56aab58a58d6483b93eb4debad [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"
12
gio3cdee592024-04-17 10:15:56 +040013 "github.com/go-git/go-billy/v5"
14 "sigs.k8s.io/yaml"
15)
16
17//go:embed values-tmpl
18var valuesTmpls embed.FS
19
gio44f621b2024-04-29 09:44:38 +040020var storeEnvAppConfigs = []string{
gio3cdee592024-04-17 10:15:56 +040021 "values-tmpl/url-shortener.cue",
gio44f621b2024-04-29 09:44:38 +040022 "values-tmpl/matrix.cue",
23 "values-tmpl/vaultwarden.cue",
gio18d5c682024-05-02 10:30:57 +040024 // "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040025 "values-tmpl/gerrit.cue",
26 "values-tmpl/jenkins.cue",
27 "values-tmpl/zot.cue",
gio18d5c682024-05-02 10:30:57 +040028 // "values-tmpl/penpot.cue",
gio44f621b2024-04-29 09:44:38 +040029 "values-tmpl/soft-serve.cue",
30 "values-tmpl/pihole.cue",
31 // "values-tmpl/maddy.cue",
gio18d5c682024-05-02 10:30:57 +040032 // "values-tmpl/qbittorrent.cue",
33 // "values-tmpl/jellyfin.cue",
gio44f621b2024-04-29 09:44:38 +040034 "values-tmpl/rpuppy.cue",
35}
36
37var envAppConfigs = []string{
gio3cdee592024-04-17 10:15:56 +040038 "values-tmpl/certificate-issuer-private.cue",
39 "values-tmpl/certificate-issuer-public.cue",
40 "values-tmpl/appmanager.cue",
41 "values-tmpl/core-auth.cue",
42 "values-tmpl/headscale-user.cue",
43 "values-tmpl/metallb-ipaddresspool.cue",
44 "values-tmpl/private-network.cue",
45 "values-tmpl/welcome.cue",
46 "values-tmpl/memberships.cue",
47 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040048 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040049 "values-tmpl/env-dns.cue",
gio09a3e5b2024-04-26 14:11:06 +040050 "values-tmpl/launcher.cue",
gio3cdee592024-04-17 10:15:56 +040051}
52
53var infraAppConfigs = []string{
54 "values-tmpl/cert-manager.cue",
55 "values-tmpl/config-repo.cue",
56 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040057 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040058 "values-tmpl/env-manager.cue",
59 "values-tmpl/fluxcd-reconciler.cue",
60 "values-tmpl/headscale-controller.cue",
61 "values-tmpl/ingress-public.cue",
62 "values-tmpl/resource-renderer-controller.cue",
63 "values-tmpl/hydra-maester.cue",
64}
65
66type AppRepository interface {
67 GetAll() ([]App, error)
68 Find(name string) (App, error)
69}
70
71type InMemoryAppRepository struct {
72 apps []App
73}
74
75func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
76 return InMemoryAppRepository{apps}
77}
78
79func (r InMemoryAppRepository) Find(name string) (App, error) {
80 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040081 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040082 return a, nil
83 }
84 }
85 return nil, fmt.Errorf("Application not found: %s", name)
86}
87
88func (r InMemoryAppRepository) GetAll() ([]App, error) {
89 return r.apps, nil
90}
91
92func CreateAllApps() []App {
93 return append(
94 createInfraApps(),
gio44f621b2024-04-29 09:44:38 +040095 append(
96 CreateEnvApps(storeEnvAppConfigs),
97 CreateEnvApps(envAppConfigs)...,
98 )...,
gio3cdee592024-04-17 10:15:56 +040099 )
100}
101
102func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400103 return CreateEnvApps(storeEnvAppConfigs)
104}
105
106func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400107 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400108 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400109 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400110 if err != nil {
111 panic(err)
112 }
gio308105e2024-04-19 13:12:13 +0400113 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400114 "base.cue": []byte(cueBaseConfig),
115 "global.cue": []byte(cueEnvAppGlobal),
116 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400117 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400118 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400119 panic(err)
120 } else {
121 ret = append(ret, app)
122 }
123 }
124 return ret
125}
126
127func createInfraApps() []App {
128 ret := make([]App, 0)
129 for _, cfgFile := range infraAppConfigs {
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 := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400135 "base.cue": []byte(cueBaseConfig),
136 "global.cue": []byte(cueInfraAppGlobal),
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
148type httpAppRepository struct {
149 apps []App
150}
151
152type appVersion struct {
153 Version string `json:"version"`
154 Urls []string `json:"urls"`
155}
156
157type allAppsResp struct {
158 ApiVersion string `json:"apiVersion"`
159 Entries map[string][]appVersion `json:"entries"`
160}
161
162func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
163 resp, err := http.Get(addr)
164 if err != nil {
165 return err
166 }
167 b, err := io.ReadAll(resp.Body)
168 if err != nil {
169 return err
170 }
171 var apps allAppsResp
172 if err := yaml.Unmarshal(b, &apps); err != nil {
173 return err
174 }
175 for name, conf := range apps.Entries {
176 for _, version := range conf {
177 resp, err := http.Get(version.Urls[0])
178 if err != nil {
179 return err
180 }
181 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
182 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
183 return err
184 }
185 sub, err := fs.Chroot(nameVersion)
186 if err != nil {
187 return err
188 }
189 if err := extractApp(resp.Body, sub); err != nil {
190 return err
191 }
192 }
193 }
194 return nil
195}
196
197func extractApp(archive io.Reader, fs billy.Filesystem) error {
198 uncompressed, err := gzip.NewReader(archive)
199 if err != nil {
200 return err
201 }
202 tarReader := tar.NewReader(uncompressed)
203 for true {
204 header, err := tarReader.Next()
205 if err == io.EOF {
206 break
207 }
208 if err != nil {
209 return err
210 }
211 switch header.Typeflag {
212 case tar.TypeDir:
213 if err := fs.MkdirAll(header.Name, 0755); err != nil {
214 return err
215 }
216 case tar.TypeReg:
217 out, err := fs.Create(header.Name)
218 if err != nil {
219 return err
220 }
221 defer out.Close()
222 if _, err := io.Copy(out, tarReader); err != nil {
223 return err
224 }
225 default:
226 return fmt.Errorf("Uknown type: %s", header.Name)
227 }
228 }
229 return nil
230}
231
232type fsAppRepository struct {
233 InMemoryAppRepository
234 fs billy.Filesystem
235}
236
237func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
238 all, err := fs.ReadDir(".")
239 if err != nil {
240 return nil, err
241 }
242 apps := make([]App, 0)
243 for _, e := range all {
244 if !e.IsDir() {
245 continue
246 }
247 appFS, err := fs.Chroot(e.Name())
248 if err != nil {
249 return nil, err
250 }
251 app, err := loadApp(appFS)
252 if err != nil {
253 log.Printf("Ignoring directory %s: %s", e.Name(), err)
254 continue
255 }
256 apps = append(apps, app)
257 }
258 return &fsAppRepository{
259 NewInMemoryAppRepository(apps),
260 fs,
261 }, nil
262}
263
264func loadApp(fs billy.Filesystem) (App, error) {
265 items, err := fs.ReadDir(".")
266 if err != nil {
267 return nil, err
268 }
269 var contents bytes.Buffer
270 for _, i := range items {
271 if i.IsDir() {
272 continue
273 }
274 f, err := fs.Open(i.Name())
275 if err != nil {
276 return nil, err
277 }
278 defer f.Close()
279 if _, err := io.Copy(&contents, f); err != nil {
280 return nil, err
281 }
282 }
gio308105e2024-04-19 13:12:13 +0400283 return NewCueEnvApp(CueAppData{
284 "base.cue": []byte(cueBaseConfig),
285 "app.cue": contents.Bytes(),
286 })
gio3cdee592024-04-17 10:15:56 +0400287}
288
gio308105e2024-04-19 13:12:13 +0400289// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
290// contents, err := fs.ReadFile(f)
291// if err != nil {
292// return nil, err
293// }
294// return processCueConfig(string(contents))
295// }
gio3cdee592024-04-17 10:15:56 +0400296
gio308105e2024-04-19 13:12:13 +0400297// func processCueConfig(contents string) (*cue.Value, error) {
298// ctx := cuecontext.New()
299// cfg := ctx.CompileString(contents + cueBaseConfig)
300// if err := cfg.Err(); err != nil {
301// return nil, err
302// }
303// if err := cfg.Validate(); err != nil {
304// return nil, err
305// }
306// return &cfg, nil
307// }
gio3cdee592024-04-17 10:15:56 +0400308
309// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
310// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
311// if err != nil {
312// panic(err)
313// }
314// return StoreApp{
315// App{
316// "maddy",
317// []string{"app-maddy"},
318// []*template.Template{
319// tmpls.Lookup("maddy.yaml"),
320// },
321// schema,
322// nil,
323// nil,
324// },
325// `<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>`,
326// "SMPT/IMAP server to communicate via email.",
327// }
328// }
329
330func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
331 app, err := r.Find(name)
332 if err != nil {
333 return nil, err
334 }
335 if a, ok := app.(EnvApp); ok {
336 return a, nil
337 } else {
338 return nil, fmt.Errorf("not found")
339 }
340}
341
342func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
343 app, err := r.Find(name)
344 if err != nil {
345 return nil, err
346 }
347 if a, ok := app.(InfraApp); ok {
348 return a, nil
349 } else {
350 return nil, fmt.Errorf("not found")
351 }
352}