blob: 6acde703b7325c71ce7ce40f363d916fc72b6060 [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
20var storeAppConfigs = []string{
21 "values-tmpl/jellyfin.cue",
22 // "values-tmpl/maddy.cue",
23 "values-tmpl/matrix.cue",
24 "values-tmpl/penpot.cue",
25 "values-tmpl/pihole.cue",
26 "values-tmpl/qbittorrent.cue",
27 "values-tmpl/rpuppy.cue",
28 "values-tmpl/soft-serve.cue",
29 "values-tmpl/vaultwarden.cue",
30 "values-tmpl/url-shortener.cue",
31 "values-tmpl/gerrit.cue",
32 "values-tmpl/jenkins.cue",
33 "values-tmpl/zot.cue",
gioc9161872024-04-21 10:46:35 +040034 "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040035 // TODO(gio): should be part of env infra
36 "values-tmpl/certificate-issuer-private.cue",
37 "values-tmpl/certificate-issuer-public.cue",
38 "values-tmpl/appmanager.cue",
39 "values-tmpl/core-auth.cue",
40 "values-tmpl/headscale-user.cue",
41 "values-tmpl/metallb-ipaddresspool.cue",
42 "values-tmpl/private-network.cue",
43 "values-tmpl/welcome.cue",
44 "values-tmpl/memberships.cue",
45 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040046 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040047 "values-tmpl/env-dns.cue",
gio3cdee592024-04-17 10:15:56 +040048}
49
50var infraAppConfigs = []string{
51 "values-tmpl/cert-manager.cue",
52 "values-tmpl/config-repo.cue",
53 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040054 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040055 "values-tmpl/env-manager.cue",
56 "values-tmpl/fluxcd-reconciler.cue",
57 "values-tmpl/headscale-controller.cue",
58 "values-tmpl/ingress-public.cue",
59 "values-tmpl/resource-renderer-controller.cue",
60 "values-tmpl/hydra-maester.cue",
61}
62
63type AppRepository interface {
64 GetAll() ([]App, error)
65 Find(name string) (App, error)
66}
67
68type InMemoryAppRepository struct {
69 apps []App
70}
71
72func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
73 return InMemoryAppRepository{apps}
74}
75
76func (r InMemoryAppRepository) Find(name string) (App, error) {
77 for _, a := range r.apps {
78 if a.Name() == name {
79 return a, nil
80 }
81 }
82 return nil, fmt.Errorf("Application not found: %s", name)
83}
84
85func (r InMemoryAppRepository) GetAll() ([]App, error) {
86 return r.apps, nil
87}
88
89func CreateAllApps() []App {
90 return append(
91 createInfraApps(),
92 CreateStoreApps()...,
93 )
94}
95
96func CreateStoreApps() []App {
97 ret := make([]App, 0)
98 for _, cfgFile := range storeAppConfigs {
gio308105e2024-04-19 13:12:13 +040099 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400100 if err != nil {
101 panic(err)
102 }
gio308105e2024-04-19 13:12:13 +0400103 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400104 "base.cue": []byte(cueBaseConfig),
105 "global.cue": []byte(cueEnvAppGlobal),
106 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400107 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400108 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400109 panic(err)
110 } else {
111 ret = append(ret, app)
112 }
113 }
114 return ret
115}
116
117func createInfraApps() []App {
118 ret := make([]App, 0)
119 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400120 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400121 if err != nil {
122 panic(err)
123 }
gio308105e2024-04-19 13:12:13 +0400124 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400125 "base.cue": []byte(cueBaseConfig),
126 "global.cue": []byte(cueInfraAppGlobal),
127 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400128 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400129 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400130 panic(err)
131 } else {
132 ret = append(ret, app)
133 }
134 }
135 return ret
136}
137
138type httpAppRepository struct {
139 apps []App
140}
141
142type appVersion struct {
143 Version string `json:"version"`
144 Urls []string `json:"urls"`
145}
146
147type allAppsResp struct {
148 ApiVersion string `json:"apiVersion"`
149 Entries map[string][]appVersion `json:"entries"`
150}
151
152func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
153 resp, err := http.Get(addr)
154 if err != nil {
155 return err
156 }
157 b, err := io.ReadAll(resp.Body)
158 if err != nil {
159 return err
160 }
161 var apps allAppsResp
162 if err := yaml.Unmarshal(b, &apps); err != nil {
163 return err
164 }
165 for name, conf := range apps.Entries {
166 for _, version := range conf {
167 resp, err := http.Get(version.Urls[0])
168 if err != nil {
169 return err
170 }
171 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
172 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
173 return err
174 }
175 sub, err := fs.Chroot(nameVersion)
176 if err != nil {
177 return err
178 }
179 if err := extractApp(resp.Body, sub); err != nil {
180 return err
181 }
182 }
183 }
184 return nil
185}
186
187func extractApp(archive io.Reader, fs billy.Filesystem) error {
188 uncompressed, err := gzip.NewReader(archive)
189 if err != nil {
190 return err
191 }
192 tarReader := tar.NewReader(uncompressed)
193 for true {
194 header, err := tarReader.Next()
195 if err == io.EOF {
196 break
197 }
198 if err != nil {
199 return err
200 }
201 switch header.Typeflag {
202 case tar.TypeDir:
203 if err := fs.MkdirAll(header.Name, 0755); err != nil {
204 return err
205 }
206 case tar.TypeReg:
207 out, err := fs.Create(header.Name)
208 if err != nil {
209 return err
210 }
211 defer out.Close()
212 if _, err := io.Copy(out, tarReader); err != nil {
213 return err
214 }
215 default:
216 return fmt.Errorf("Uknown type: %s", header.Name)
217 }
218 }
219 return nil
220}
221
222type fsAppRepository struct {
223 InMemoryAppRepository
224 fs billy.Filesystem
225}
226
227func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
228 all, err := fs.ReadDir(".")
229 if err != nil {
230 return nil, err
231 }
232 apps := make([]App, 0)
233 for _, e := range all {
234 if !e.IsDir() {
235 continue
236 }
237 appFS, err := fs.Chroot(e.Name())
238 if err != nil {
239 return nil, err
240 }
241 app, err := loadApp(appFS)
242 if err != nil {
243 log.Printf("Ignoring directory %s: %s", e.Name(), err)
244 continue
245 }
246 apps = append(apps, app)
247 }
248 return &fsAppRepository{
249 NewInMemoryAppRepository(apps),
250 fs,
251 }, nil
252}
253
254func loadApp(fs billy.Filesystem) (App, error) {
255 items, err := fs.ReadDir(".")
256 if err != nil {
257 return nil, err
258 }
259 var contents bytes.Buffer
260 for _, i := range items {
261 if i.IsDir() {
262 continue
263 }
264 f, err := fs.Open(i.Name())
265 if err != nil {
266 return nil, err
267 }
268 defer f.Close()
269 if _, err := io.Copy(&contents, f); err != nil {
270 return nil, err
271 }
272 }
gio308105e2024-04-19 13:12:13 +0400273 return NewCueEnvApp(CueAppData{
274 "base.cue": []byte(cueBaseConfig),
275 "app.cue": contents.Bytes(),
276 })
gio3cdee592024-04-17 10:15:56 +0400277}
278
gio308105e2024-04-19 13:12:13 +0400279// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
280// contents, err := fs.ReadFile(f)
281// if err != nil {
282// return nil, err
283// }
284// return processCueConfig(string(contents))
285// }
gio3cdee592024-04-17 10:15:56 +0400286
gio308105e2024-04-19 13:12:13 +0400287// func processCueConfig(contents string) (*cue.Value, error) {
288// ctx := cuecontext.New()
289// cfg := ctx.CompileString(contents + cueBaseConfig)
290// if err := cfg.Err(); err != nil {
291// return nil, err
292// }
293// if err := cfg.Validate(); err != nil {
294// return nil, err
295// }
296// return &cfg, nil
297// }
gio3cdee592024-04-17 10:15:56 +0400298
299// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
300// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
301// if err != nil {
302// panic(err)
303// }
304// return StoreApp{
305// App{
306// "maddy",
307// []string{"app-maddy"},
308// []*template.Template{
309// tmpls.Lookup("maddy.yaml"),
310// },
311// schema,
312// nil,
313// nil,
314// },
315// `<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>`,
316// "SMPT/IMAP server to communicate via email.",
317// }
318// }
319
320func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
321 app, err := r.Find(name)
322 if err != nil {
323 return nil, err
324 }
325 if a, ok := app.(EnvApp); ok {
326 return a, nil
327 } else {
328 return nil, fmt.Errorf("not found")
329 }
330}
331
332func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
333 app, err := r.Find(name)
334 if err != nil {
335 return nil, err
336 }
337 if a, ok := app.(InfraApp); ok {
338 return a, nil
339 } else {
340 return nil, fmt.Errorf("not found")
341 }
342}