blob: 5727e64665cd0fa9283dd72a5b86f3e7b0486b8b [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",
24 "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",
gio44f621b2024-04-29 09:44:38 +040028 "values-tmpl/penpot.cue",
29 "values-tmpl/soft-serve.cue",
30 "values-tmpl/pihole.cue",
31 // "values-tmpl/maddy.cue",
32 "values-tmpl/qbittorrent.cue",
33 "values-tmpl/jellyfin.cue",
34 "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",
gio3cdee592024-04-17 10:15:56 +040050}
51
52var infraAppConfigs = []string{
53 "values-tmpl/cert-manager.cue",
54 "values-tmpl/config-repo.cue",
55 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040056 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040057 "values-tmpl/env-manager.cue",
58 "values-tmpl/fluxcd-reconciler.cue",
59 "values-tmpl/headscale-controller.cue",
60 "values-tmpl/ingress-public.cue",
61 "values-tmpl/resource-renderer-controller.cue",
62 "values-tmpl/hydra-maester.cue",
63}
64
65type AppRepository interface {
66 GetAll() ([]App, error)
67 Find(name string) (App, error)
68}
69
70type InMemoryAppRepository struct {
71 apps []App
72}
73
74func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
75 return InMemoryAppRepository{apps}
76}
77
78func (r InMemoryAppRepository) Find(name string) (App, error) {
79 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040080 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040081 return a, nil
82 }
83 }
84 return nil, fmt.Errorf("Application not found: %s", name)
85}
86
87func (r InMemoryAppRepository) GetAll() ([]App, error) {
88 return r.apps, nil
89}
90
91func CreateAllApps() []App {
92 return append(
93 createInfraApps(),
gio44f621b2024-04-29 09:44:38 +040094 append(
95 CreateEnvApps(storeEnvAppConfigs),
96 CreateEnvApps(envAppConfigs)...,
97 )...,
gio3cdee592024-04-17 10:15:56 +040098 )
99}
100
101func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400102 return CreateEnvApps(storeEnvAppConfigs)
103}
104
105func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400106 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400107 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400108 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400109 if err != nil {
110 panic(err)
111 }
gio308105e2024-04-19 13:12:13 +0400112 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400113 "base.cue": []byte(cueBaseConfig),
114 "global.cue": []byte(cueEnvAppGlobal),
115 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400116 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400117 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400118 panic(err)
119 } else {
120 ret = append(ret, app)
121 }
122 }
123 return ret
124}
125
126func createInfraApps() []App {
127 ret := make([]App, 0)
128 for _, cfgFile := range infraAppConfigs {
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 := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400134 "base.cue": []byte(cueBaseConfig),
135 "global.cue": []byte(cueInfraAppGlobal),
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
147type httpAppRepository struct {
148 apps []App
149}
150
151type appVersion struct {
152 Version string `json:"version"`
153 Urls []string `json:"urls"`
154}
155
156type allAppsResp struct {
157 ApiVersion string `json:"apiVersion"`
158 Entries map[string][]appVersion `json:"entries"`
159}
160
161func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
162 resp, err := http.Get(addr)
163 if err != nil {
164 return err
165 }
166 b, err := io.ReadAll(resp.Body)
167 if err != nil {
168 return err
169 }
170 var apps allAppsResp
171 if err := yaml.Unmarshal(b, &apps); err != nil {
172 return err
173 }
174 for name, conf := range apps.Entries {
175 for _, version := range conf {
176 resp, err := http.Get(version.Urls[0])
177 if err != nil {
178 return err
179 }
180 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
181 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
182 return err
183 }
184 sub, err := fs.Chroot(nameVersion)
185 if err != nil {
186 return err
187 }
188 if err := extractApp(resp.Body, sub); err != nil {
189 return err
190 }
191 }
192 }
193 return nil
194}
195
196func extractApp(archive io.Reader, fs billy.Filesystem) error {
197 uncompressed, err := gzip.NewReader(archive)
198 if err != nil {
199 return err
200 }
201 tarReader := tar.NewReader(uncompressed)
202 for true {
203 header, err := tarReader.Next()
204 if err == io.EOF {
205 break
206 }
207 if err != nil {
208 return err
209 }
210 switch header.Typeflag {
211 case tar.TypeDir:
212 if err := fs.MkdirAll(header.Name, 0755); err != nil {
213 return err
214 }
215 case tar.TypeReg:
216 out, err := fs.Create(header.Name)
217 if err != nil {
218 return err
219 }
220 defer out.Close()
221 if _, err := io.Copy(out, tarReader); err != nil {
222 return err
223 }
224 default:
225 return fmt.Errorf("Uknown type: %s", header.Name)
226 }
227 }
228 return nil
229}
230
231type fsAppRepository struct {
232 InMemoryAppRepository
233 fs billy.Filesystem
234}
235
236func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
237 all, err := fs.ReadDir(".")
238 if err != nil {
239 return nil, err
240 }
241 apps := make([]App, 0)
242 for _, e := range all {
243 if !e.IsDir() {
244 continue
245 }
246 appFS, err := fs.Chroot(e.Name())
247 if err != nil {
248 return nil, err
249 }
250 app, err := loadApp(appFS)
251 if err != nil {
252 log.Printf("Ignoring directory %s: %s", e.Name(), err)
253 continue
254 }
255 apps = append(apps, app)
256 }
257 return &fsAppRepository{
258 NewInMemoryAppRepository(apps),
259 fs,
260 }, nil
261}
262
263func loadApp(fs billy.Filesystem) (App, error) {
264 items, err := fs.ReadDir(".")
265 if err != nil {
266 return nil, err
267 }
268 var contents bytes.Buffer
269 for _, i := range items {
270 if i.IsDir() {
271 continue
272 }
273 f, err := fs.Open(i.Name())
274 if err != nil {
275 return nil, err
276 }
277 defer f.Close()
278 if _, err := io.Copy(&contents, f); err != nil {
279 return nil, err
280 }
281 }
gio308105e2024-04-19 13:12:13 +0400282 return NewCueEnvApp(CueAppData{
283 "base.cue": []byte(cueBaseConfig),
284 "app.cue": contents.Bytes(),
285 })
gio3cdee592024-04-17 10:15:56 +0400286}
287
gio308105e2024-04-19 13:12:13 +0400288// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
289// contents, err := fs.ReadFile(f)
290// if err != nil {
291// return nil, err
292// }
293// return processCueConfig(string(contents))
294// }
gio3cdee592024-04-17 10:15:56 +0400295
gio308105e2024-04-19 13:12:13 +0400296// func processCueConfig(contents string) (*cue.Value, error) {
297// ctx := cuecontext.New()
298// cfg := ctx.CompileString(contents + cueBaseConfig)
299// if err := cfg.Err(); err != nil {
300// return nil, err
301// }
302// if err := cfg.Validate(); err != nil {
303// return nil, err
304// }
305// return &cfg, nil
306// }
gio3cdee592024-04-17 10:15:56 +0400307
308// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
309// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
310// if err != nil {
311// panic(err)
312// }
313// return StoreApp{
314// App{
315// "maddy",
316// []string{"app-maddy"},
317// []*template.Template{
318// tmpls.Lookup("maddy.yaml"),
319// },
320// schema,
321// nil,
322// nil,
323// },
324// `<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>`,
325// "SMPT/IMAP server to communicate via email.",
326// }
327// }
328
329func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
330 app, err := r.Find(name)
331 if err != nil {
332 return nil, err
333 }
334 if a, ok := app.(EnvApp); ok {
335 return a, nil
336 } else {
337 return nil, fmt.Errorf("not found")
338 }
339}
340
341func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
342 app, err := r.Find(name)
343 if err != nil {
344 return nil, err
345 }
346 if a, ok := app.(InfraApp); ok {
347 return a, nil
348 } else {
349 return nil, fmt.Errorf("not found")
350 }
351}