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