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