blob: df35ae86ee38186464b94bbe0709b56166fe2d60 [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
gio44f621b2024-04-29 09:44:38 +040020var storeEnvAppConfigs = []string{
gio0eaf2712024-04-14 13:08:46 +040021 "values-tmpl/dodo-app.cue",
gio3cdee592024-04-17 10:15:56 +040022 "values-tmpl/url-shortener.cue",
gio44f621b2024-04-29 09:44:38 +040023 "values-tmpl/matrix.cue",
24 "values-tmpl/vaultwarden.cue",
gio18d5c682024-05-02 10:30:57 +040025 // "values-tmpl/open-project.cue",
gio3cdee592024-04-17 10:15:56 +040026 "values-tmpl/gerrit.cue",
27 "values-tmpl/jenkins.cue",
28 "values-tmpl/zot.cue",
gio18d5c682024-05-02 10:30:57 +040029 // "values-tmpl/penpot.cue",
gio44f621b2024-04-29 09:44:38 +040030 "values-tmpl/soft-serve.cue",
31 "values-tmpl/pihole.cue",
32 // "values-tmpl/maddy.cue",
gio18d5c682024-05-02 10:30:57 +040033 // "values-tmpl/qbittorrent.cue",
34 // "values-tmpl/jellyfin.cue",
gio44f621b2024-04-29 09:44:38 +040035 "values-tmpl/rpuppy.cue",
giocb34ad22024-07-11 08:01:13 +040036 "values-tmpl/certificate-issuer-custom.cue",
gio44f621b2024-04-29 09:44:38 +040037}
38
39var envAppConfigs = []string{
gio266c04f2024-07-03 14:18:45 +040040 "values-tmpl/dodo-app-instance.cue",
gio3cdee592024-04-17 10:15:56 +040041 "values-tmpl/certificate-issuer-private.cue",
42 "values-tmpl/certificate-issuer-public.cue",
43 "values-tmpl/appmanager.cue",
44 "values-tmpl/core-auth.cue",
45 "values-tmpl/headscale-user.cue",
46 "values-tmpl/metallb-ipaddresspool.cue",
47 "values-tmpl/private-network.cue",
48 "values-tmpl/welcome.cue",
49 "values-tmpl/memberships.cue",
50 "values-tmpl/headscale.cue",
Davit Tabidze56f86a42024-04-09 19:15:25 +040051 "values-tmpl/launcher.cue",
gioe72b54f2024-04-22 10:44:41 +040052 "values-tmpl/env-dns.cue",
gio09a3e5b2024-04-26 14:11:06 +040053 "values-tmpl/launcher.cue",
gio3cdee592024-04-17 10:15:56 +040054}
55
56var infraAppConfigs = []string{
57 "values-tmpl/cert-manager.cue",
58 "values-tmpl/config-repo.cue",
59 "values-tmpl/csi-driver-smb.cue",
gioe72b54f2024-04-22 10:44:41 +040060 "values-tmpl/dns-gateway.cue",
gio3cdee592024-04-17 10:15:56 +040061 "values-tmpl/env-manager.cue",
62 "values-tmpl/fluxcd-reconciler.cue",
63 "values-tmpl/headscale-controller.cue",
64 "values-tmpl/ingress-public.cue",
65 "values-tmpl/resource-renderer-controller.cue",
66 "values-tmpl/hydra-maester.cue",
67}
68
69type AppRepository interface {
70 GetAll() ([]App, error)
71 Find(name string) (App, error)
72}
73
74type InMemoryAppRepository struct {
75 apps []App
76}
77
78func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
79 return InMemoryAppRepository{apps}
80}
81
82func (r InMemoryAppRepository) Find(name string) (App, error) {
83 for _, a := range r.apps {
gio44f621b2024-04-29 09:44:38 +040084 if a.Slug() == name {
gio3cdee592024-04-17 10:15:56 +040085 return a, nil
86 }
87 }
88 return nil, fmt.Errorf("Application not found: %s", name)
89}
90
91func (r InMemoryAppRepository) GetAll() ([]App, error) {
92 return r.apps, nil
93}
94
95func CreateAllApps() []App {
96 return append(
97 createInfraApps(),
gio44f621b2024-04-29 09:44:38 +040098 append(
99 CreateEnvApps(storeEnvAppConfigs),
100 CreateEnvApps(envAppConfigs)...,
101 )...,
gio3cdee592024-04-17 10:15:56 +0400102 )
103}
104
105func CreateStoreApps() []App {
gio44f621b2024-04-29 09:44:38 +0400106 return CreateEnvApps(storeEnvAppConfigs)
107}
108
109func CreateEnvApps(configs []string) []App {
gio3cdee592024-04-17 10:15:56 +0400110 ret := make([]App, 0)
gio44f621b2024-04-29 09:44:38 +0400111 for _, cfgFile := range configs {
gio308105e2024-04-19 13:12:13 +0400112 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400113 if err != nil {
114 panic(err)
115 }
gio308105e2024-04-19 13:12:13 +0400116 if app, err := NewCueEnvApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400117 "base.cue": []byte(cueBaseConfig),
118 "global.cue": []byte(cueEnvAppGlobal),
119 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400120 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400121 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400122 panic(err)
123 } else {
124 ret = append(ret, app)
125 }
126 }
127 return ret
128}
129
130func createInfraApps() []App {
131 ret := make([]App, 0)
132 for _, cfgFile := range infraAppConfigs {
gio308105e2024-04-19 13:12:13 +0400133 contents, err := valuesTmpls.ReadFile(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400134 if err != nil {
135 panic(err)
136 }
gio308105e2024-04-19 13:12:13 +0400137 if app, err := NewCueInfraApp(CueAppData{
gioe72b54f2024-04-22 10:44:41 +0400138 "base.cue": []byte(cueBaseConfig),
139 "global.cue": []byte(cueInfraAppGlobal),
140 "app.cue": contents,
gio308105e2024-04-19 13:12:13 +0400141 }); err != nil {
gioe72b54f2024-04-22 10:44:41 +0400142 fmt.Println(cfgFile)
gio3cdee592024-04-17 10:15:56 +0400143 panic(err)
144 } else {
145 ret = append(ret, app)
146 }
147 }
148 return ret
149}
150
151type httpAppRepository struct {
152 apps []App
153}
154
155type appVersion struct {
156 Version string `json:"version"`
157 Urls []string `json:"urls"`
158}
159
160type allAppsResp struct {
161 ApiVersion string `json:"apiVersion"`
162 Entries map[string][]appVersion `json:"entries"`
163}
164
165func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
166 resp, err := http.Get(addr)
167 if err != nil {
168 return err
169 }
170 b, err := io.ReadAll(resp.Body)
171 if err != nil {
172 return err
173 }
174 var apps allAppsResp
175 if err := yaml.Unmarshal(b, &apps); err != nil {
176 return err
177 }
178 for name, conf := range apps.Entries {
179 for _, version := range conf {
180 resp, err := http.Get(version.Urls[0])
181 if err != nil {
182 return err
183 }
184 nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
185 if err := fs.MkdirAll(nameVersion, 0700); err != nil {
186 return err
187 }
188 sub, err := fs.Chroot(nameVersion)
189 if err != nil {
190 return err
191 }
192 if err := extractApp(resp.Body, sub); err != nil {
193 return err
194 }
195 }
196 }
197 return nil
198}
199
200func extractApp(archive io.Reader, fs billy.Filesystem) error {
201 uncompressed, err := gzip.NewReader(archive)
202 if err != nil {
203 return err
204 }
205 tarReader := tar.NewReader(uncompressed)
206 for true {
207 header, err := tarReader.Next()
208 if err == io.EOF {
209 break
210 }
211 if err != nil {
212 return err
213 }
214 switch header.Typeflag {
215 case tar.TypeDir:
216 if err := fs.MkdirAll(header.Name, 0755); err != nil {
217 return err
218 }
219 case tar.TypeReg:
220 out, err := fs.Create(header.Name)
221 if err != nil {
222 return err
223 }
224 defer out.Close()
225 if _, err := io.Copy(out, tarReader); err != nil {
226 return err
227 }
228 default:
229 return fmt.Errorf("Uknown type: %s", header.Name)
230 }
231 }
232 return nil
233}
234
235type fsAppRepository struct {
236 InMemoryAppRepository
237 fs billy.Filesystem
238}
239
240func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
241 all, err := fs.ReadDir(".")
242 if err != nil {
243 return nil, err
244 }
245 apps := make([]App, 0)
246 for _, e := range all {
247 if !e.IsDir() {
248 continue
249 }
250 appFS, err := fs.Chroot(e.Name())
251 if err != nil {
252 return nil, err
253 }
254 app, err := loadApp(appFS)
255 if err != nil {
256 log.Printf("Ignoring directory %s: %s", e.Name(), err)
257 continue
258 }
259 apps = append(apps, app)
260 }
261 return &fsAppRepository{
262 NewInMemoryAppRepository(apps),
263 fs,
264 }, nil
265}
266
267func loadApp(fs billy.Filesystem) (App, error) {
268 items, err := fs.ReadDir(".")
269 if err != nil {
270 return nil, err
271 }
272 var contents bytes.Buffer
273 for _, i := range items {
274 if i.IsDir() {
275 continue
276 }
277 f, err := fs.Open(i.Name())
278 if err != nil {
279 return nil, err
280 }
281 defer f.Close()
282 if _, err := io.Copy(&contents, f); err != nil {
283 return nil, err
284 }
285 }
gio308105e2024-04-19 13:12:13 +0400286 return NewCueEnvApp(CueAppData{
287 "base.cue": []byte(cueBaseConfig),
288 "app.cue": contents.Bytes(),
289 })
gio3cdee592024-04-17 10:15:56 +0400290}
291
gio308105e2024-04-19 13:12:13 +0400292// func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
293// contents, err := fs.ReadFile(f)
294// if err != nil {
295// return nil, err
296// }
297// return processCueConfig(string(contents))
298// }
gio3cdee592024-04-17 10:15:56 +0400299
gio308105e2024-04-19 13:12:13 +0400300// func processCueConfig(contents string) (*cue.Value, error) {
301// ctx := cuecontext.New()
302// cfg := ctx.CompileString(contents + cueBaseConfig)
303// if err := cfg.Err(); err != nil {
304// return nil, err
305// }
306// if err := cfg.Validate(); err != nil {
307// return nil, err
308// }
309// return &cfg, nil
310// }
gio3cdee592024-04-17 10:15:56 +0400311
312// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
313// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
314// if err != nil {
315// panic(err)
316// }
317// return StoreApp{
318// App{
319// "maddy",
320// []string{"app-maddy"},
321// []*template.Template{
322// tmpls.Lookup("maddy.yaml"),
323// },
324// schema,
325// nil,
326// nil,
327// },
328// `<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>`,
329// "SMPT/IMAP server to communicate via email.",
330// }
331// }
332
333func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
334 app, err := r.Find(name)
335 if err != nil {
336 return nil, err
337 }
338 if a, ok := app.(EnvApp); ok {
339 return a, nil
340 } else {
341 return nil, fmt.Errorf("not found")
342 }
343}
344
345func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
346 app, err := r.Find(name)
347 if err != nil {
348 return nil, err
349 }
350 if a, ok := app.(InfraApp); ok {
351 return a, nil
352 } else {
353 return nil, fmt.Errorf("not found")
354 }
355}