blob: 70cdb8e79ef6c442d8d4fb685f1389c6347bf219 [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
13 "cuelang.org/go/cue"
14 "cuelang.org/go/cue/cuecontext"
15 "github.com/go-git/go-billy/v5"
16 "sigs.k8s.io/yaml"
17)
18
19//go:embed values-tmpl
20var valuesTmpls embed.FS
21
22var storeAppConfigs = []string{
23 "values-tmpl/jellyfin.cue",
24 // "values-tmpl/maddy.cue",
25 "values-tmpl/matrix.cue",
26 "values-tmpl/penpot.cue",
27 "values-tmpl/pihole.cue",
28 "values-tmpl/qbittorrent.cue",
29 "values-tmpl/rpuppy.cue",
30 "values-tmpl/soft-serve.cue",
31 "values-tmpl/vaultwarden.cue",
32 "values-tmpl/url-shortener.cue",
33 "values-tmpl/gerrit.cue",
34 "values-tmpl/jenkins.cue",
35 "values-tmpl/zot.cue",
36 // TODO(gio): should be part of env infra
37 "values-tmpl/certificate-issuer-private.cue",
38 "values-tmpl/certificate-issuer-public.cue",
39 "values-tmpl/appmanager.cue",
40 "values-tmpl/core-auth.cue",
41 "values-tmpl/headscale-user.cue",
42 "values-tmpl/metallb-ipaddresspool.cue",
43 "values-tmpl/private-network.cue",
44 "values-tmpl/welcome.cue",
45 "values-tmpl/memberships.cue",
46 "values-tmpl/headscale.cue",
47}
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 {
98 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
99 if err != nil {
100 panic(err)
101 }
102 if app, err := NewCueEnvApp(cfg); err != nil {
103 panic(err)
104 } else {
105 ret = append(ret, app)
106 }
107 }
108 return ret
109}
110
111func createInfraApps() []App {
112 ret := make([]App, 0)
113 for _, cfgFile := range infraAppConfigs {
114 cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
115 if err != nil {
116 panic(err)
117 }
118 if app, err := NewCueInfraApp(cfg); err != nil {
119 panic(err)
120 } else {
121 ret = append(ret, app)
122 }
123 }
124 return ret
125}
126
127type httpAppRepository struct {
128 apps []App
129}
130
131type appVersion struct {
132 Version string `json:"version"`
133 Urls []string `json:"urls"`
134}
135
136type allAppsResp struct {
137 ApiVersion string `json:"apiVersion"`
138 Entries map[string][]appVersion `json:"entries"`
139}
140
141func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
142 resp, err := http.Get(addr)
143 if err != nil {
144 return err
145 }
146 b, err := io.ReadAll(resp.Body)
147 if err != nil {
148 return err
149 }
150 var apps allAppsResp
151 if err := yaml.Unmarshal(b, &apps); err != nil {
152 return err
153 }
154 for name, conf := range apps.Entries {
155 for _, version := range conf {
156 resp, err := http.Get(version.Urls[0])
157 if err != nil {
158 return err
159 }
160 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
161 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
162 return err
163 }
164 sub, err := fs.Chroot(nameVersion)
165 if err != nil {
166 return err
167 }
168 if err := extractApp(resp.Body, sub); err != nil {
169 return err
170 }
171 }
172 }
173 return nil
174}
175
176func extractApp(archive io.Reader, fs billy.Filesystem) error {
177 uncompressed, err := gzip.NewReader(archive)
178 if err != nil {
179 return err
180 }
181 tarReader := tar.NewReader(uncompressed)
182 for true {
183 header, err := tarReader.Next()
184 if err == io.EOF {
185 break
186 }
187 if err != nil {
188 return err
189 }
190 switch header.Typeflag {
191 case tar.TypeDir:
192 if err := fs.MkdirAll(header.Name, 0755); err != nil {
193 return err
194 }
195 case tar.TypeReg:
196 out, err := fs.Create(header.Name)
197 if err != nil {
198 return err
199 }
200 defer out.Close()
201 if _, err := io.Copy(out, tarReader); err != nil {
202 return err
203 }
204 default:
205 return fmt.Errorf("Uknown type: %s", header.Name)
206 }
207 }
208 return nil
209}
210
211type fsAppRepository struct {
212 InMemoryAppRepository
213 fs billy.Filesystem
214}
215
216func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
217 all, err := fs.ReadDir(".")
218 if err != nil {
219 return nil, err
220 }
221 apps := make([]App, 0)
222 for _, e := range all {
223 if !e.IsDir() {
224 continue
225 }
226 appFS, err := fs.Chroot(e.Name())
227 if err != nil {
228 return nil, err
229 }
230 app, err := loadApp(appFS)
231 if err != nil {
232 log.Printf("Ignoring directory %s: %s", e.Name(), err)
233 continue
234 }
235 apps = append(apps, app)
236 }
237 return &fsAppRepository{
238 NewInMemoryAppRepository(apps),
239 fs,
240 }, nil
241}
242
243func loadApp(fs billy.Filesystem) (App, error) {
244 items, err := fs.ReadDir(".")
245 if err != nil {
246 return nil, err
247 }
248 var contents bytes.Buffer
249 for _, i := range items {
250 if i.IsDir() {
251 continue
252 }
253 f, err := fs.Open(i.Name())
254 if err != nil {
255 return nil, err
256 }
257 defer f.Close()
258 if _, err := io.Copy(&contents, f); err != nil {
259 return nil, err
260 }
261 }
262 cfg, err := processCueConfig(contents.String())
263 if err != nil {
264 return nil, err
265 }
266 return NewCueEnvApp(cfg)
267}
268
269func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
270 contents, err := fs.ReadFile(f)
271 if err != nil {
272 return nil, err
273 }
274 return processCueConfig(string(contents))
275}
276
277func processCueConfig(contents string) (*cue.Value, error) {
278 ctx := cuecontext.New()
279 cfg := ctx.CompileString(contents + cueBaseConfig)
280 if err := cfg.Err(); err != nil {
281 return nil, err
282 }
283 if err := cfg.Validate(); err != nil {
284 return nil, err
285 }
286 return &cfg, nil
287}
288
289// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
290// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
291// if err != nil {
292// panic(err)
293// }
294// return StoreApp{
295// App{
296// "maddy",
297// []string{"app-maddy"},
298// []*template.Template{
299// tmpls.Lookup("maddy.yaml"),
300// },
301// schema,
302// nil,
303// nil,
304// },
305// `<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>`,
306// "SMPT/IMAP server to communicate via email.",
307// }
308// }
309
310func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
311 app, err := r.Find(name)
312 if err != nil {
313 return nil, err
314 }
315 if a, ok := app.(EnvApp); ok {
316 return a, nil
317 } else {
318 return nil, fmt.Errorf("not found")
319 }
320}
321
322func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
323 app, err := r.Find(name)
324 if err != nil {
325 return nil, err
326 }
327 if a, ok := app.(InfraApp); ok {
328 return a, nil
329 } else {
330 return nil, fmt.Errorf("not found")
331 }
332}