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