installer: separate infra and repo apps. make network configurable
diff --git a/core/installer/app.go b/core/installer/app.go
index 8d79c58..4ed1549 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -12,6 +12,10 @@
//go:embed values-tmpl
var valuesTmpls embed.FS
+type Named interface {
+ Nam() string
+}
+
type App struct {
Name string
Namespaces []string
@@ -20,31 +24,45 @@
Readme *template.Template
}
-type AppRepository interface {
- GetAll() ([]App, error)
- Find(name string) (*App, error)
+type StoreApp struct {
+ App
+ Icon string
+ ShortDescription string
}
-type InMemoryAppRepository struct {
- apps []App
+func (a App) Nam() string {
+ return a.Name
}
-func NewInMemoryAppRepository(apps []App) AppRepository {
- return &InMemoryAppRepository{
+func (a StoreApp) Nam() string {
+ return a.Name
+}
+
+type AppRepository[A Named] interface {
+ GetAll() ([]A, error)
+ Find(name string) (*A, error)
+}
+
+type InMemoryAppRepository[A Named] struct {
+ apps []A
+}
+
+func NewInMemoryAppRepository[A Named](apps []A) AppRepository[A] {
+ return &InMemoryAppRepository[A]{
apps,
}
}
-func (r InMemoryAppRepository) Find(name string) (*App, error) {
+func (r InMemoryAppRepository[A]) Find(name string) (*A, error) {
for _, a := range r.apps {
- if a.Name == name {
+ if a.Nam() == name {
return &a, nil
}
}
return nil, fmt.Errorf("Application not found: %s", name)
}
-func (r InMemoryAppRepository) GetAll() ([]App, error) {
+func (r InMemoryAppRepository[A]) GetAll() ([]A, error) {
return r.apps, nil
}
@@ -53,18 +71,11 @@
if err != nil {
log.Fatal(err)
}
- return []App{
+ ret := []App{
CreateAppIngressPrivate(valuesTmpls, tmpls),
CreateCertificateIssuerPublic(valuesTmpls, tmpls),
CreateCertificateIssuerPrivate(valuesTmpls, tmpls),
CreateAppCoreAuth(valuesTmpls, tmpls),
- CreateAppVaultwarden(valuesTmpls, tmpls),
- CreateAppMatrix(valuesTmpls, tmpls),
- CreateAppPihole(valuesTmpls, tmpls),
- CreateAppMaddy(valuesTmpls, tmpls),
- CreateAppQBittorrent(valuesTmpls, tmpls),
- CreateAppJellyfin(valuesTmpls, tmpls),
- CreateAppRpuppy(valuesTmpls, tmpls),
CreateAppHeadscale(valuesTmpls, tmpls),
CreateAppTailscaleProxy(valuesTmpls, tmpls),
CreateMetallbConfigEnv(valuesTmpls, tmpls),
@@ -78,6 +89,26 @@
CreateResourceRendererController(valuesTmpls, tmpls),
CreateHeadscaleController(valuesTmpls, tmpls),
}
+ for _, a := range CreateStoreApps() {
+ ret = append(ret, a.App)
+ }
+ return ret
+}
+
+func CreateStoreApps() []StoreApp {
+ tmpls, err := template.New("root").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(valuesTmpls, "values-tmpl/*")
+ if err != nil {
+ log.Fatal(err)
+ }
+ return []StoreApp{
+ CreateAppVaultwarden(valuesTmpls, tmpls),
+ CreateAppMatrix(valuesTmpls, tmpls),
+ CreateAppPihole(valuesTmpls, tmpls),
+ CreateAppMaddy(valuesTmpls, tmpls),
+ CreateAppQBittorrent(valuesTmpls, tmpls),
+ CreateAppJellyfin(valuesTmpls, tmpls),
+ CreateAppRpuppy(valuesTmpls, tmpls),
+ }
}
// TODO(gio): service account needs permission to create/update secret
@@ -146,116 +177,144 @@
}
}
-func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) App {
+func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/vaultwarden.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "vaultwarden",
- []string{"app-vaultwarden"},
- []*template.Template{
- tmpls.Lookup("vaultwarden.yaml"),
+ return StoreApp{
+ App: App{
+ "vaultwarden",
+ []string{"app-vaultwarden"},
+ []*template.Template{
+ tmpls.Lookup("vaultwarden.yaml"),
+ },
+ string(schema),
+ tmpls.Lookup("vaultwarden.md"),
},
- string(schema),
- tmpls.Lookup("vaultwarden.md"),
+ Icon: "arcticons:bitwarden",
+ ShortDescription: "Open source implementation of Bitwarden password manager. Can be used with official client applications.",
}
}
-func CreateAppMatrix(fs embed.FS, tmpls *template.Template) App {
+func CreateAppMatrix(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/matrix.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "matrix",
- []string{"app-matrix"},
- []*template.Template{
- tmpls.Lookup("matrix-storage.yaml"),
- tmpls.Lookup("matrix.yaml"),
+ return StoreApp{
+ App{
+ "matrix",
+ []string{"app-matrix"},
+ []*template.Template{
+ tmpls.Lookup("matrix-storage.yaml"),
+ tmpls.Lookup("matrix.yaml"),
+ },
+ string(schema),
+ nil,
},
- string(schema),
- nil,
+ "simple-icons:matrix",
+ "An open network for secure, decentralised communication",
}
}
-func CreateAppPihole(fs embed.FS, tmpls *template.Template) App {
+func CreateAppPihole(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/pihole.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "pihole",
- []string{"app-pihole"},
- []*template.Template{
- tmpls.Lookup("pihole.yaml"),
+ return StoreApp{
+ App{
+ "pihole",
+ []string{"app-pihole"},
+ []*template.Template{
+ tmpls.Lookup("pihole.yaml"),
+ },
+ string(schema),
+ tmpls.Lookup("pihole.md"),
},
- string(schema),
- tmpls.Lookup("pihole.md"),
+ "simple-icons:pihole",
+ "Pi-hole is a Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole and optionally a DHCP server, intended for use on a private network.",
}
}
-func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
+func CreateAppMaddy(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/maddy.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "maddy",
- []string{"app-maddy"},
- []*template.Template{
- tmpls.Lookup("maddy.yaml"),
+ return StoreApp{
+ App{
+ "maddy",
+ []string{"app-maddy"},
+ []*template.Template{
+ tmpls.Lookup("maddy.yaml"),
+ },
+ string(schema),
+ nil,
},
- string(schema),
- nil,
+ "arcticons:huawei-email",
+ "SMPT/IMAP server to communicate via email.",
}
}
-func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) App {
+func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/qbittorrent.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "qbittorrent",
- []string{"app-qbittorrent"},
- []*template.Template{
- tmpls.Lookup("qbittorrent.yaml"),
+ return StoreApp{
+ App{
+ "qbittorrent",
+ []string{"app-qbittorrent"},
+ []*template.Template{
+ tmpls.Lookup("qbittorrent.yaml"),
+ },
+ string(schema),
+ tmpls.Lookup("qbittorrent.md"),
},
- string(schema),
- tmpls.Lookup("qbittorrent.md"),
+ "arcticons:qbittorrent-remote",
+ "qBittorrent is a cross-platform free and open-source BitTorrent client written in native C++. It relies on Boost, Qt 6 toolkit and the libtorrent-rasterbar library, with an optional search engine written in Python.",
}
}
-func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) App {
+func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/jellyfin.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "jellyfin",
- []string{"app-jellyfin"},
- []*template.Template{
- tmpls.Lookup("jellyfin.yaml"),
+ return StoreApp{
+ App{
+ "jellyfin",
+ []string{"app-jellyfin"},
+ []*template.Template{
+ tmpls.Lookup("jellyfin.yaml"),
+ },
+ string(schema),
+ nil,
},
- string(schema),
- nil,
+ "arcticons:jellyfin",
+ "Jellyfin is a free and open-source media server and suite of multimedia applications designed to organize, manage, and share digital media files to networked devices.",
}
}
-func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) App {
+func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) StoreApp {
schema, err := fs.ReadFile("values-tmpl/rpuppy.jsonschema")
if err != nil {
panic(err)
}
- return App{
- "rpuppy",
- []string{"app-rpuppy"},
- []*template.Template{
- tmpls.Lookup("rpuppy.yaml"),
+ return StoreApp{
+ App{
+ "rpuppy",
+ []string{"app-rpuppy"},
+ []*template.Template{
+ tmpls.Lookup("rpuppy.yaml"),
+ },
+ string(schema),
+ tmpls.Lookup("rpuppy.md"),
},
- string(schema),
- tmpls.Lookup("rpuppy.md"),
+ "ph:dog-thin",
+ "Delights users with randomly generate puppy pictures. Can be configured to be reachable only from private network or publicly.",
}
}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index d2feccc..b5815e4 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -72,5 +72,6 @@
"Namespace": namespaces[0],
}
}
+ // TODO(giolekva): use ns suffix for app directory
return m.repoIO.InstallApp(app, "apps", all)
}
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index cff270e..9a15c6e 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -68,7 +68,7 @@
if err != nil {
return err
}
- kube, err := installer.NewOutOfClusterNamespaceCreator(rootFlags.kubeConfig)
+ kube, err := newNSCreator()
if err != nil {
return err
}
@@ -79,7 +79,7 @@
if err != nil {
return err
}
- r := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+ r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
s := &server{
port: appManagerFlags.port,
m: m,
@@ -92,7 +92,7 @@
type server struct {
port int
m *installer.AppManager
- r installer.AppRepository
+ r installer.AppRepository[installer.StoreApp]
}
func (s *server) start() {
@@ -113,10 +113,12 @@
}
type app struct {
- Name string `json:"name"`
- Slug string `json:"slug"`
- Schema string `json:"schema"`
- Config map[string]any `json:"config"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ ShortDescription string `json:"shortDescription"`
+ Slug string `json:"slug"`
+ Schema string `json:"schema"`
+ Config any `json:"config"`
}
func (s *server) handleAppRepo(c echo.Context) error {
@@ -127,7 +129,7 @@
resp := make([]app, len(all))
for i, a := range all {
config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
- resp[i] = app{a.Name, a.Name, a.Schema, config}
+ resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config}
}
return c.JSON(http.StatusOK, resp)
}
@@ -139,7 +141,7 @@
return err
}
config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
- return c.JSON(http.StatusOK, app{a.Name, a.Name, a.Schema, config})
+ return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]})
}
type file struct {
@@ -152,6 +154,29 @@
Files []file `json:"files"`
}
+type network struct {
+ Name string
+ IngressClass string
+ CertificateIssuer string
+ Domain string
+}
+
+func createNetworks(global installer.Config) []network {
+ return []network{
+ {
+ Name: "Public",
+ IngressClass: fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName),
+ CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id),
+ Domain: global.Values.Domain,
+ },
+ {
+ Name: "Private",
+ IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
+ Domain: global.Values.PrivateDomain,
+ },
+ }
+}
+
func (s *server) handleAppRender(c echo.Context) error {
slug := c.Param("slug")
contents, err := ioutil.ReadAll(c.Request().Body)
@@ -166,6 +191,13 @@
if err := json.Unmarshal(contents, &values); err != nil {
return err
}
+ if network, ok := values["Network"]; ok {
+ for _, n := range createNetworks(global) {
+ if n.Name == network { // TODO(giolekva): handle not found
+ values["Network"] = n
+ }
+ }
+ }
all := map[string]any{
"Global": global.Values,
"Values": values,
@@ -218,11 +250,18 @@
if err != nil {
return err
}
+ if network, ok := values["Network"]; ok {
+ for _, n := range createNetworks(config) {
+ if n.Name == network { // TODO(giolekva): handle not found
+ values["Network"] = n
+ }
+ }
+ }
nsGen := installer.NewCombine(
installer.NewPrefixGenerator(config.Values.Id+"-"),
installer.NewRandomSuffixGenerator(3),
)
- if err := s.m.Install(*a, nsGen, values); err != nil {
+ if err := s.m.Install(a.App, nsGen, values); err != nil {
return err
}
return c.String(http.StatusOK, "Installed")