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