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