blob: 88b4d8887e5feea49531a00416590407bbbf4e3c [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",
gio3cdee592024-04-17 10:15:56 +040047}
48
49var infraAppConfigs = []string{
50 "values-tmpl/cert-manager.cue",
51 "values-tmpl/config-repo.cue",
52 "values-tmpl/csi-driver-smb.cue",
53 "values-tmpl/dns-zone-manager.cue",
54 "values-tmpl/env-manager.cue",
55 "values-tmpl/fluxcd-reconciler.cue",
56 "values-tmpl/headscale-controller.cue",
57 "values-tmpl/ingress-public.cue",
58 "values-tmpl/resource-renderer-controller.cue",
59 "values-tmpl/hydra-maester.cue",
60}
61
62type AppRepository interface {
63 GetAll() ([]App, error)
64 Find(name string) (App, error)
65}
66
67type InMemoryAppRepository struct {
68 apps []App
69}
70
71func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
72 return InMemoryAppRepository{apps}
73}
74
75func (r InMemoryAppRepository) Find(name string) (App, error) {
76 for _, a := range r.apps {
77 if a.Name() == name {
78 return a, nil
79 }
80 }
81 return nil, fmt.Errorf("Application not found: %s", name)
82}
83
84func (r InMemoryAppRepository) GetAll() ([]App, error) {
85 return r.apps, nil
86}
87
88func CreateAllApps() []App {
89 return append(
90 createInfraApps(),
91 CreateStoreApps()...,
92 )
93}
94
95func CreateStoreApps() []App {
96 ret := make([]App, 0)
97 for _, cfgFile := range storeAppConfigs {
gio308105e2024-04-19 13:12:13 +040098 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +040099 if err != nil {
100 panic(err)
101 }
gio308105e2024-04-19 13:12:13 +0400102 if app, err := NewCueEnvApp(CueAppData{
103 "base.cue": []byte(cueBaseConfig),
104 "app.cue": contents,
105 }); err != nil {
gio3cdee592024-04-17 10:15:56 +0400106 panic(err)
107 } else {
108 ret = append(ret, app)
109 }
110 }
111 return ret
112}
113
114func createInfraApps() []App {
115 ret := make([]App, 0)
116 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400117 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400118 if err != nil {
119 panic(err)
120 }
gio308105e2024-04-19 13:12:13 +0400121 if app, err := NewCueInfraApp(CueAppData{
122 "base.cue": []byte(cueBaseConfig),
123 "app.cue": contents,
124 }); err != nil {
gio3cdee592024-04-17 10:15:56 +0400125 panic(err)
126 } else {
127 ret = append(ret, app)
128 }
129 }
130 return ret
131}
132
133type httpAppRepository struct {
134 apps []App
135}
136
137type appVersion struct {
138 Version string `json:"version"`
139 Urls []string `json:"urls"`
140}
141
142type allAppsResp struct {
143 ApiVersion string `json:"apiVersion"`
144 Entries map[string][]appVersion `json:"entries"`
145}
146
147func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
148 resp, err := http.Get(addr)
149 if err != nil {
150 return err
151 }
152 b, err := io.ReadAll(resp.Body)
153 if err != nil {
154 return err
155 }
156 var apps allAppsResp
157 if err := yaml.Unmarshal(b, &apps); err != nil {
158 return err
159 }
160 for name, conf := range apps.Entries {
161 for _, version := range conf {
162 resp, err := http.Get(version.Urls[0])
163 if err != nil {
164 return err
165 }
166 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
167 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
168 return err
169 }
170 sub, err := fs.Chroot(nameVersion)
171 if err != nil {
172 return err
173 }
174 if err := extractApp(resp.Body, sub); err != nil {
175 return err
176 }
177 }
178 }
179 return nil
180}
181
182func extractApp(archive io.Reader, fs billy.Filesystem) error {
183 uncompressed, err := gzip.NewReader(archive)
184 if err != nil {
185 return err
186 }
187 tarReader := tar.NewReader(uncompressed)
188 for true {
189 header, err := tarReader.Next()
190 if err == io.EOF {
191 break
192 }
193 if err != nil {
194 return err
195 }
196 switch header.Typeflag {
197 case tar.TypeDir:
198 if err := fs.MkdirAll(header.Name, 0755); err != nil {
199 return err
200 }
201 case tar.TypeReg:
202 out, err := fs.Create(header.Name)
203 if err != nil {
204 return err
205 }
206 defer out.Close()
207 if _, err := io.Copy(out, tarReader); err != nil {
208 return err
209 }
210 default:
211 return fmt.Errorf("Uknown type: %s", header.Name)
212 }
213 }
214 return nil
215}
216
217type fsAppRepository struct {
218 InMemoryAppRepository
219 fs billy.Filesystem
220}
221
222func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
223 all, err := fs.ReadDir(".")
224 if err != nil {
225 return nil, err
226 }
227 apps := make([]App, 0)
228 for _, e := range all {
229 if !e.IsDir() {
230 continue
231 }
232 appFS, err := fs.Chroot(e.Name())
233 if err != nil {
234 return nil, err
235 }
236 app, err := loadApp(appFS)
237 if err != nil {
238 log.Printf("Ignoring directory %s: %s", e.Name(), err)
239 continue
240 }
241 apps = append(apps, app)
242 }
243 return &fsAppRepository{
244 NewInMemoryAppRepository(apps),
245 fs,
246 }, nil
247}
248
249func loadApp(fs billy.Filesystem) (App, error) {
250 items, err := fs.ReadDir(".")
251 if err != nil {
252 return nil, err
253 }
254 var contents bytes.Buffer
255 for _, i := range items {
256 if i.IsDir() {
257 continue
258 }
259 f, err := fs.Open(i.Name())
260 if err != nil {
261 return nil, err
262 }
263 defer f.Close()
264 if _, err := io.Copy(&contents, f); err != nil {
265 return nil, err
266 }
267 }
gio308105e2024-04-19 13:12:13 +0400268 return NewCueEnvApp(CueAppData{
269 "base.cue": []byte(cueBaseConfig),
270 "app.cue": contents.Bytes(),
271 })
gio3cdee592024-04-17 10:15:56 +0400272}
273
gio308105e2024-04-19 13:12:13 +0400274// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
275// contents, err := fs.ReadFile(f)
276// if err != nil {
277// return nil, err
278// }
279// return processCueConfig(string(contents))
280// }
gio3cdee592024-04-17 10:15:56 +0400281
gio308105e2024-04-19 13:12:13 +0400282// func processCueConfig(contents string) (*cue.Value, error) {
283// ctx := cuecontext.New()
284// cfg := ctx.CompileString(contents + cueBaseConfig)
285// if err := cfg.Err(); err != nil {
286// return nil, err
287// }
288// if err := cfg.Validate(); err != nil {
289// return nil, err
290// }
291// return &cfg, nil
292// }
gio3cdee592024-04-17 10:15:56 +0400293
294// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
295// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
296// if err != nil {
297// panic(err)
298// }
299// return StoreApp{
300// App{
301// "maddy",
302// []string{"app-maddy"},
303// []*template.Template{
304// tmpls.Lookup("maddy.yaml"),
305// },
306// schema,
307// nil,
308// nil,
309// },
310// `<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>`,
311// "SMPT/IMAP server to communicate via email.",
312// }
313// }
314
315func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
316 app, err := r.Find(name)
317 if err != nil {
318 return nil, err
319 }
320 if a, ok := app.(EnvApp); ok {
321 return a, nil
322 } else {
323 return nil, fmt.Errorf("not found")
324 }
325}
326
327func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
328 app, err := r.Find(name)
329 if err != nil {
330 return nil, err
331 }
332 if a, ok := app.(InfraApp); ok {
333 return a, nil
334 } else {
335 return nil, fmt.Errorf("not found")
336 }
337}