Installer: Separate infrastructure and environment apps.
Have two separate application managers, one for installing apps on the
dodo infra, and nother installing on individual environments.
Change-Id: I1b24f008e30c5533c48c22ea92328bc4bb7abc54
diff --git a/core/installer/Makefile b/core/installer/Makefile
index e36755b..332a889 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -26,7 +26,7 @@
/usr/local/go/bin/go test -v ./...
bootstrap:
- ./pcloud --kubeconfig=../../priv/kubeconfig-hetzner bootstrap --env-name=dodo --charts-dir=../../charts --admin-pub-key=/Users/lekva/.ssh/id_rsa.pub --from-ip=192.168.100.210 --to-ip=192.168.100.240 --storage-dir=/pcloud-storage/longhorn
+ ./pcloud --kubeconfig=../../priv/kubeconfig-hetzner bootstrap --env-name=dodo --charts-dir=../../charts --admin-pub-key=/Users/lekva/.ssh/id_rsa.pub --from-ip=192.168.100.210 --to-ip=192.168.100.240 --storage-dir=/pcloud-storage/longhorn --public-ip=135.181.48.180,65.108.39.172
create_env:
./pcloud --kubeconfig=../../priv/kubeconfig create-env --admin-priv-key=/Users/lekva/.ssh/id_rsa --name=lekva --ip=192.168.0.211 --admin-username=gio
diff --git a/core/installer/app.go b/core/installer/app.go
index 35c6d11..18f9e75 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -1,67 +1,17 @@
package installer
import (
- "archive/tar"
"bytes"
- "compress/gzip"
- "embed"
"encoding/json"
"fmt"
template "html/template"
- "io"
- "log"
- "net/http"
+ "net"
"strings"
"cuelang.org/go/cue"
- "cuelang.org/go/cue/cuecontext"
cueyaml "cuelang.org/go/encoding/yaml"
- "github.com/go-git/go-billy/v5"
- "sigs.k8s.io/yaml"
)
-//go:embed values-tmpl
-var valuesTmpls embed.FS
-
-var storeAppConfigs = []string{
- "values-tmpl/jellyfin.cue",
- // "values-tmpl/maddy.cue",
- "values-tmpl/matrix.cue",
- "values-tmpl/penpot.cue",
- "values-tmpl/pihole.cue",
- "values-tmpl/qbittorrent.cue",
- "values-tmpl/rpuppy.cue",
- "values-tmpl/soft-serve.cue",
- "values-tmpl/vaultwarden.cue",
- "values-tmpl/url-shortener.cue",
- "values-tmpl/gerrit.cue",
- "values-tmpl/jenkins.cue",
- "values-tmpl/zot.cue",
-}
-
-var infraAppConfigs = []string{
- "values-tmpl/appmanager.cue",
- "values-tmpl/cert-manager.cue",
- "values-tmpl/certificate-issuer-private.cue",
- "values-tmpl/certificate-issuer-public.cue",
- "values-tmpl/config-repo.cue",
- "values-tmpl/core-auth.cue",
- "values-tmpl/csi-driver-smb.cue",
- "values-tmpl/dns-zone-manager.cue",
- "values-tmpl/env-manager.cue",
- "values-tmpl/fluxcd-reconciler.cue",
- "values-tmpl/headscale-controller.cue",
- "values-tmpl/headscale-user.cue",
- "values-tmpl/headscale.cue",
- "values-tmpl/ingress-public.cue",
- "values-tmpl/metallb-ipaddresspool.cue",
- "values-tmpl/private-network.cue",
- "values-tmpl/resource-renderer-controller.cue",
- "values-tmpl/welcome.cue",
- "values-tmpl/memberships.cue",
- "values-tmpl/hydra-maester.cue",
-}
-
// TODO(gio): import
const cueBaseConfig = `
name: string | *""
@@ -131,6 +81,7 @@
}
#Release: {
+ appInstanceId: string
namespace: string
repoAddr: string
appDir: string
@@ -309,27 +260,64 @@
}
`
-type appConfig struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Description string `json:"description"`
- Namespaces []string `json:"namespaces"`
- Icon template.HTML `json:"icon"`
-}
-
type Rendered struct {
+ Name string
Readme string
Resources map[string][]byte
Ports []PortForward
+ Config AppInstanceConfig
}
+type PortForward struct {
+ Allocator string `json:"allocator"`
+ Protocol string `json:"protocol"`
+ SourcePort int `json:"sourcePort"`
+ TargetService string `json:"targetService"`
+ TargetPort int `json:"targetPort"`
+}
+
+type AppType int
+
+const (
+ AppTypeInfra AppType = iota
+ AppTypeEnv
+)
+
type App interface {
+ Type() AppType
Name() string
Description() string
Icon() template.HTML
Schema() Schema
Namespace() string
- Render(derived Derived) (Rendered, error)
+}
+
+type InfraConfig struct {
+ Name string `json:"pcloudEnvName"` // #TODO(gio): change to name
+ PublicIP []net.IP `json:"publicIP"`
+ InfraNamespacePrefix string `json:"namespacePrefix"`
+ InfraAdminPublicKey []byte `json:"infraAdminPublicKey"`
+}
+
+type InfraApp interface {
+ App
+ Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error)
+}
+
+// TODO(gio): rename to EnvConfig
+type AppEnvConfig struct {
+ Id string `json:"id"`
+ InfraName string `json:"pcloudEnvName"`
+ Domain string `json:"domain"`
+ PrivateDomain string `json:"privateDomain"`
+ ContactEmail string `json:"contactEmail"`
+ PublicIP []net.IP `json:"publicIP"`
+ NamespacePrefix string `json:"namespacePrefix"`
+}
+
+type EnvApp interface {
+ App
+ Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error)
}
type cueApp struct {
@@ -341,18 +329,16 @@
cfg *cue.Value
}
-type cueAppConfig struct {
- Name string `json:"name"`
- Namespace string `json:"namespace"`
- Description string `json:"description"`
- Icon string `json:"icon"`
-}
-
func newCueApp(config *cue.Value) (cueApp, error) {
if config == nil {
return cueApp{}, fmt.Errorf("config not provided")
}
- var cfg cueAppConfig
+ cfg := struct {
+ Name string `json:"name"`
+ Namespace string `json:"namespace"`
+ Description string `json:"description"`
+ Icon string `json:"icon"`
+ }{}
if err := config.Decode(&cfg); err != nil {
return cueApp{}, err
}
@@ -390,21 +376,14 @@
return a.namespace
}
-type PortForward struct {
- Allocator string `json:"allocator"`
- Protocol string `json:"protocol"`
- SourcePort int `json:"sourcePort"`
- TargetService string `json:"targetService"`
- TargetPort int `json:"targetPort"`
-}
-
-func (a cueApp) Render(derived Derived) (Rendered, error) {
+func (a cueApp) render(values map[string]any) (Rendered, error) {
ret := Rendered{
+ Name: a.Name(),
Resources: make(map[string][]byte),
Ports: make([]PortForward, 0),
}
var buf bytes.Buffer
- if err := json.NewEncoder(&buf).Encode(derived); err != nil {
+ if err := json.NewEncoder(&buf).Encode(values); err != nil {
return Rendered{}, err
}
ctx := a.cfg.Context()
@@ -440,254 +419,70 @@
return ret, nil
}
-type AppRepository interface {
- GetAll() ([]App, error)
- Find(name string) (App, error)
+type cueEnvApp struct {
+ cueApp
}
-type InMemoryAppRepository struct {
- apps []App
-}
-
-func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
- return InMemoryAppRepository{apps}
-}
-
-func (r InMemoryAppRepository) Find(name string) (App, error) {
- for _, a := range r.apps {
- if a.Name() == name {
- return a, nil
- }
- }
- return nil, fmt.Errorf("Application not found: %s", name)
-}
-
-func (r InMemoryAppRepository) GetAll() ([]App, error) {
- return r.apps, nil
-}
-
-func CreateAllApps() []App {
- return append(
- createApps(infraAppConfigs),
- CreateStoreApps()...,
- )
-}
-
-func CreateStoreApps() []App {
- return createApps(storeAppConfigs)
-}
-
-func createApps(configs []string) []App {
- ret := make([]App, 0)
- for _, cfgFile := range configs {
- cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
- if err != nil {
- panic(err)
- }
- if app, err := newCueApp(cfg); err != nil {
- panic(err)
- } else {
- ret = append(ret, app)
- }
- }
- return ret
-}
-
-// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
-// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
-// if err != nil {
-// panic(err)
-// }
-// return StoreApp{
-// App{
-// "maddy",
-// []string{"app-maddy"},
-// []*template.Template{
-// tmpls.Lookup("maddy.yaml"),
-// },
-// schema,
-// nil,
-// nil,
-// },
-// `<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>`,
-// "SMPT/IMAP server to communicate via email.",
-// }
-// }
-
-type httpAppRepository struct {
- apps []App
-}
-
-type appVersion struct {
- Version string `json:"version"`
- Urls []string `json:"urls"`
-}
-
-type allAppsResp struct {
- ApiVersion string `json:"apiVersion"`
- Entries map[string][]appVersion `json:"entries"`
-}
-
-func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
- resp, err := http.Get(addr)
- if err != nil {
- return err
- }
- b, err := io.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- var apps allAppsResp
- if err := yaml.Unmarshal(b, &apps); err != nil {
- return err
- }
- for name, conf := range apps.Entries {
- for _, version := range conf {
- resp, err := http.Get(version.Urls[0])
- if err != nil {
- return err
- }
- nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
- if err := fs.MkdirAll(nameVersion, 0700); err != nil {
- return err
- }
- sub, err := fs.Chroot(nameVersion)
- if err != nil {
- return err
- }
- if err := extractApp(resp.Body, sub); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func extractApp(archive io.Reader, fs billy.Filesystem) error {
- uncompressed, err := gzip.NewReader(archive)
- if err != nil {
- return err
- }
- tarReader := tar.NewReader(uncompressed)
- for true {
- header, err := tarReader.Next()
- if err == io.EOF {
- break
- }
- if err != nil {
- return err
- }
- switch header.Typeflag {
- case tar.TypeDir:
- if err := fs.MkdirAll(header.Name, 0755); err != nil {
- return err
- }
- case tar.TypeReg:
- out, err := fs.Create(header.Name)
- if err != nil {
- return err
- }
- defer out.Close()
- if _, err := io.Copy(out, tarReader); err != nil {
- return err
- }
- default:
- return fmt.Errorf("Uknown type: %s", header.Name)
- }
- }
- return nil
-}
-
-type fsAppRepository struct {
- InMemoryAppRepository
- fs billy.Filesystem
-}
-
-func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
- all, err := fs.ReadDir(".")
+func NewCueEnvApp(config *cue.Value) (EnvApp, error) {
+ app, err := newCueApp(config)
if err != nil {
return nil, err
}
- apps := make([]App, 0)
- for _, e := range all {
- if !e.IsDir() {
- continue
- }
- appFS, err := fs.Chroot(e.Name())
- if err != nil {
- return nil, err
- }
- app, err := loadApp(appFS)
- if err != nil {
- log.Printf("Ignoring directory %s: %s", e.Name(), err)
- continue
- }
- apps = append(apps, app)
- }
- return &fsAppRepository{
- NewInMemoryAppRepository(apps),
- fs,
- }, nil
+ return cueEnvApp{app}, nil
}
-func loadApp(fs billy.Filesystem) (App, error) {
- items, err := fs.ReadDir(".")
+func (a cueEnvApp) Type() AppType {
+ return AppTypeEnv
+}
+
+func (a cueEnvApp) Render(release Release, env AppEnvConfig, values map[string]any) (Rendered, error) {
+ networks := CreateNetworks(env)
+ derived, err := deriveValues(values, a.Schema(), networks)
+ if err != nil {
+ return Rendered{}, nil
+ }
+ ret, err := a.cueApp.render(map[string]any{
+ "global": env,
+ "release": release,
+ "input": derived,
+ })
+ if err != nil {
+ return Rendered{}, err
+ }
+ ret.Config = AppInstanceConfig{
+ AppId: a.Name(),
+ Env: env,
+ Release: release,
+ Values: values,
+ Input: derived,
+ }
+ return ret, nil
+}
+
+type cueInfraApp struct {
+ cueApp
+}
+
+func NewCueInfraApp(config *cue.Value) (InfraApp, error) {
+ app, err := newCueApp(config)
if err != nil {
return nil, err
}
- var contents bytes.Buffer
- for _, i := range items {
- if i.IsDir() {
- continue
- }
- f, err := fs.Open(i.Name())
- if err != nil {
- return nil, err
- }
- defer f.Close()
- if _, err := io.Copy(&contents, f); err != nil {
- return nil, err
- }
- }
- cfg, err := processCueConfig(contents.String())
- if err != nil {
- return nil, err
- }
- return newCueApp(cfg)
+ return cueInfraApp{app}, nil
+}
+
+func (a cueInfraApp) Type() AppType {
+ return AppTypeInfra
+}
+
+func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any) (Rendered, error) {
+ return a.cueApp.render(map[string]any{
+ "global": infra,
+ "release": release,
+ "input": values,
+ })
}
func cleanName(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
}
-
-func processCueConfig(contents string) (*cue.Value, error) {
- ctx := cuecontext.New()
- cfg := ctx.CompileString(contents + cueBaseConfig)
- if err := cfg.Err(); err != nil {
- return nil, err
- }
- if err := cfg.Validate(); err != nil {
- return nil, err
- }
- return &cfg, nil
-}
-
-func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
- contents, err := fs.ReadFile(f)
- if err != nil {
- return nil, err
- }
- return processCueConfig(string(contents))
-}
-
-func createApp(fs embed.FS, configFile string) App {
- cfg, err := readCueConfigFromFile(fs, configFile)
- if err != nil {
- panic(err)
- }
- if app, err := newCueApp(cfg); err != nil {
- panic(err)
- } else {
- return app
- }
-}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index e438a26..7e89eec 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -14,7 +14,7 @@
"sigs.k8s.io/yaml"
)
-const appDir = "/apps"
+const appDirRoot = "/apps"
const configFileName = "config.yaml"
const kustomizationFileName = "kustomization.yaml"
@@ -30,32 +30,32 @@
}, nil
}
-func (m *AppManager) Config() (Config, error) {
- var cfg Config
+func (m *AppManager) Config() (AppEnvConfig, error) {
+ var cfg AppEnvConfig
if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
- return Config{}, err
+ return AppEnvConfig{}, err
} else {
return cfg, nil
}
}
-func (m *AppManager) appConfig(path string) (AppConfig, error) {
- var cfg AppConfig
+func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
+ var cfg AppInstanceConfig
if err := ReadYaml(m.repoIO, path, &cfg); err != nil {
- return AppConfig{}, err
+ return AppInstanceConfig{}, err
} else {
return cfg, nil
}
}
-func (m *AppManager) FindAllInstances(name string) ([]AppConfig, error) {
- kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
+func (m *AppManager) FindAllInstances(name string) ([]AppInstanceConfig, error) {
+ kust, err := ReadKustomization(m.repoIO, filepath.Join(appDirRoot, "kustomization.yaml"))
if err != nil {
return nil, err
}
- ret := make([]AppConfig, 0)
+ ret := make([]AppInstanceConfig, 0)
for _, app := range kust.Resources {
- cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
+ cfg, err := m.appConfig(filepath.Join(appDirRoot, app, "config.yaml"))
if err != nil {
return nil, err
}
@@ -67,34 +67,34 @@
return ret, nil
}
-func (m *AppManager) FindInstance(id string) (AppConfig, error) {
- kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
+func (m *AppManager) FindInstance(id string) (AppInstanceConfig, error) {
+ kust, err := ReadKustomization(m.repoIO, filepath.Join(appDirRoot, "kustomization.yaml"))
if err != nil {
- return AppConfig{}, err
+ return AppInstanceConfig{}, err
}
for _, app := range kust.Resources {
if app == id {
- cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
+ cfg, err := m.appConfig(filepath.Join(appDirRoot, app, "config.yaml"))
if err != nil {
- return AppConfig{}, err
+ return AppInstanceConfig{}, err
}
cfg.Id = id
return cfg, nil
}
}
- return AppConfig{}, nil
+ return AppInstanceConfig{}, nil
}
-func (m *AppManager) AppConfig(name string) (AppConfig, error) {
- configF, err := m.repoIO.Reader(filepath.Join(appDir, name, configFileName))
+func (m *AppManager) AppConfig(name string) (AppInstanceConfig, error) {
+ configF, err := m.repoIO.Reader(filepath.Join(appDirRoot, name, configFileName))
if err != nil {
- return AppConfig{}, err
+ return AppInstanceConfig{}, err
}
defer configF.Close()
- var cfg AppConfig
+ var cfg AppInstanceConfig
contents, err := ioutil.ReadAll(configF)
if err != nil {
- return AppConfig{}, err
+ return AppInstanceConfig{}, err
}
err = yaml.UnmarshalStrict(contents, &cfg)
return cfg, err
@@ -152,19 +152,8 @@
return nil
}
-func InstallApp(repo RepoIO, nsc NamespaceCreator, app App, appDir string, namespace string, initValues map[string]any, derived Derived) error {
- if err := nsc.Create(namespace); err != nil {
- return err
- }
- derived.Release = Release{
- Namespace: namespace,
- RepoAddr: repo.FullAddress(),
- AppDir: appDir,
- }
- rendered, err := app.Render(derived)
- if err != nil {
- return err
- }
+// TODO(gio): rename to CommitApp
+func InstallApp(repo RepoIO, appDir string, rendered Rendered) error {
if err := openPorts(rendered.Ports); err != nil {
return err
}
@@ -179,12 +168,7 @@
if err := r.CreateDir(appDir); err != nil {
return "", err
}
- cfg := AppConfig{
- AppId: app.Name(),
- Config: initValues,
- Derived: derived,
- }
- if err := WriteYaml(r, path.Join(appDir, configFileName), cfg); err != nil {
+ if err := WriteYaml(r, path.Join(appDir, configFileName), rendered.Config); err != nil {
return "", err
}
}
@@ -205,54 +189,61 @@
return "", err
}
}
- return fmt.Sprintf("install: %s", app.Name()), nil
+ return fmt.Sprintf("install: %s", rendered.Name), nil
})
}
-func (m *AppManager) Install(app App, appDir string, namespace string, values map[string]any) error {
+// TODO(gio): commit instanceId -> appDir mapping as well
+func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) error {
appDir = filepath.Clean(appDir)
if err := m.repoIO.Pull(); err != nil {
return err
}
- globalConfig, err := m.Config()
+ if err := m.nsCreator.Create(namespace); err != nil {
+ return err
+ }
+ env, err := m.Config()
if err != nil {
return err
}
- derivedValues, err := deriveValues(values, app.Schema(), CreateNetworks(globalConfig))
+ release := Release{
+ AppInstanceId: instanceId,
+ Namespace: namespace,
+ RepoAddr: m.repoIO.FullAddress(),
+ AppDir: appDir,
+ }
+ rendered, err := app.Render(release, env, values)
if err != nil {
return err
}
- derived := Derived{
- Global: globalConfig.Values,
- Values: derivedValues,
- }
- return InstallApp(m.repoIO, m.nsCreator, app, appDir, namespace, values, derived)
+ return InstallApp(m.repoIO, appDir, rendered)
}
-func (m *AppManager) Update(app App, instanceId string, config map[string]any) error {
+func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any) error {
if err := m.repoIO.Pull(); err != nil {
return err
}
- globalConfig, err := m.Config()
+ env, err := m.Config()
if err != nil {
return err
}
- instanceDir := filepath.Join(appDir, instanceId)
+ instanceDir := filepath.Join(appDirRoot, instanceId)
instanceConfigPath := filepath.Join(instanceDir, configFileName)
- appConfig, err := m.appConfig(instanceConfigPath)
+ config, err := m.appConfig(instanceConfigPath)
if err != nil {
return err
}
- derivedValues, err := deriveValues(config, app.Schema(), CreateNetworks(globalConfig))
+ release := Release{
+ AppInstanceId: instanceId,
+ Namespace: config.Release.Namespace,
+ RepoAddr: m.repoIO.FullAddress(),
+ AppDir: instanceDir,
+ }
+ rendered, err := app.Render(release, env, values)
if err != nil {
return err
}
- derived := Derived{
- Global: globalConfig.Values,
- Release: appConfig.Derived.Release,
- Values: derivedValues,
- }
- return InstallApp(m.repoIO, m.nsCreator, app, instanceDir, appConfig.Derived.Release.Namespace, config, derived)
+ return InstallApp(m.repoIO, instanceDir, rendered)
}
func (m *AppManager) Remove(instanceId string) error {
@@ -260,8 +251,8 @@
return err
}
return m.repoIO.Atomic(func(r RepoFS) (string, error) {
- r.RemoveDir(filepath.Join(appDir, instanceId))
- kustPath := filepath.Join(appDir, "kustomization.yaml")
+ r.RemoveDir(filepath.Join(appDirRoot, instanceId))
+ kustPath := filepath.Join(appDirRoot, "kustomization.yaml")
kust, err := ReadKustomization(r, kustPath)
if err != nil {
return "", err
@@ -273,20 +264,67 @@
}
// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
-func CreateNetworks(global Config) []Network {
+func CreateNetworks(env AppEnvConfig) []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,
- AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public/api/allocate", global.Values.PCloudEnvName),
+ IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
+ CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
+ Domain: env.Domain,
+ AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
},
{
Name: "Private",
- IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
- Domain: global.Values.PrivateDomain,
- AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private/api/allocate", global.Values.Id),
+ IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
+ Domain: env.PrivateDomain,
+ AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
},
}
}
+
+// InfraAppmanager
+
+type InfraAppManager struct {
+ repoIO RepoIO
+ nsCreator NamespaceCreator
+}
+
+func NewInfraAppManager(repoIO RepoIO, nsCreator NamespaceCreator) (*InfraAppManager, error) {
+ return &InfraAppManager{
+ repoIO,
+ nsCreator,
+ }, nil
+}
+
+func (m *InfraAppManager) Config() (InfraConfig, error) {
+ var cfg InfraConfig
+ if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
+ return InfraConfig{}, err
+ } else {
+ return cfg, nil
+ }
+}
+
+func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) error {
+ appDir = filepath.Clean(appDir)
+ if err := m.repoIO.Pull(); err != nil {
+ return err
+ }
+ if err := m.nsCreator.Create(namespace); err != nil {
+ return err
+ }
+ infra, err := m.Config()
+ if err != nil {
+ return err
+ }
+ release := Release{
+ Namespace: namespace,
+ RepoAddr: m.repoIO.FullAddress(),
+ AppDir: appDir,
+ }
+ rendered, err := app.Render(release, infra, values)
+ if err != nil {
+ return err
+ }
+ return InstallApp(m.repoIO, appDir, rendered)
+}
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
new file mode 100644
index 0000000..70cdb8e
--- /dev/null
+++ b/core/installer/app_repository.go
@@ -0,0 +1,332 @@
+package installer
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "embed"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/cuecontext"
+ "github.com/go-git/go-billy/v5"
+ "sigs.k8s.io/yaml"
+)
+
+//go:embed values-tmpl
+var valuesTmpls embed.FS
+
+var storeAppConfigs = []string{
+ "values-tmpl/jellyfin.cue",
+ // "values-tmpl/maddy.cue",
+ "values-tmpl/matrix.cue",
+ "values-tmpl/penpot.cue",
+ "values-tmpl/pihole.cue",
+ "values-tmpl/qbittorrent.cue",
+ "values-tmpl/rpuppy.cue",
+ "values-tmpl/soft-serve.cue",
+ "values-tmpl/vaultwarden.cue",
+ "values-tmpl/url-shortener.cue",
+ "values-tmpl/gerrit.cue",
+ "values-tmpl/jenkins.cue",
+ "values-tmpl/zot.cue",
+ // TODO(gio): should be part of env infra
+ "values-tmpl/certificate-issuer-private.cue",
+ "values-tmpl/certificate-issuer-public.cue",
+ "values-tmpl/appmanager.cue",
+ "values-tmpl/core-auth.cue",
+ "values-tmpl/headscale-user.cue",
+ "values-tmpl/metallb-ipaddresspool.cue",
+ "values-tmpl/private-network.cue",
+ "values-tmpl/welcome.cue",
+ "values-tmpl/memberships.cue",
+ "values-tmpl/headscale.cue",
+}
+
+var infraAppConfigs = []string{
+ "values-tmpl/cert-manager.cue",
+ "values-tmpl/config-repo.cue",
+ "values-tmpl/csi-driver-smb.cue",
+ "values-tmpl/dns-zone-manager.cue",
+ "values-tmpl/env-manager.cue",
+ "values-tmpl/fluxcd-reconciler.cue",
+ "values-tmpl/headscale-controller.cue",
+ "values-tmpl/ingress-public.cue",
+ "values-tmpl/resource-renderer-controller.cue",
+ "values-tmpl/hydra-maester.cue",
+}
+
+type AppRepository interface {
+ GetAll() ([]App, error)
+ Find(name string) (App, error)
+}
+
+type InMemoryAppRepository struct {
+ apps []App
+}
+
+func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
+ return InMemoryAppRepository{apps}
+}
+
+func (r InMemoryAppRepository) Find(name string) (App, error) {
+ for _, a := range r.apps {
+ if a.Name() == name {
+ return a, nil
+ }
+ }
+ return nil, fmt.Errorf("Application not found: %s", name)
+}
+
+func (r InMemoryAppRepository) GetAll() ([]App, error) {
+ return r.apps, nil
+}
+
+func CreateAllApps() []App {
+ return append(
+ createInfraApps(),
+ CreateStoreApps()...,
+ )
+}
+
+func CreateStoreApps() []App {
+ ret := make([]App, 0)
+ for _, cfgFile := range storeAppConfigs {
+ cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
+ if err != nil {
+ panic(err)
+ }
+ if app, err := NewCueEnvApp(cfg); err != nil {
+ panic(err)
+ } else {
+ ret = append(ret, app)
+ }
+ }
+ return ret
+}
+
+func createInfraApps() []App {
+ ret := make([]App, 0)
+ for _, cfgFile := range infraAppConfigs {
+ cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
+ if err != nil {
+ panic(err)
+ }
+ if app, err := NewCueInfraApp(cfg); err != nil {
+ panic(err)
+ } else {
+ ret = append(ret, app)
+ }
+ }
+ return ret
+}
+
+type httpAppRepository struct {
+ apps []App
+}
+
+type appVersion struct {
+ Version string `json:"version"`
+ Urls []string `json:"urls"`
+}
+
+type allAppsResp struct {
+ ApiVersion string `json:"apiVersion"`
+ Entries map[string][]appVersion `json:"entries"`
+}
+
+func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
+ resp, err := http.Get(addr)
+ if err != nil {
+ return err
+ }
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ var apps allAppsResp
+ if err := yaml.Unmarshal(b, &apps); err != nil {
+ return err
+ }
+ for name, conf := range apps.Entries {
+ for _, version := range conf {
+ resp, err := http.Get(version.Urls[0])
+ if err != nil {
+ return err
+ }
+ nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
+ if err := fs.MkdirAll(nameVersion, 0700); err != nil {
+ return err
+ }
+ sub, err := fs.Chroot(nameVersion)
+ if err != nil {
+ return err
+ }
+ if err := extractApp(resp.Body, sub); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func extractApp(archive io.Reader, fs billy.Filesystem) error {
+ uncompressed, err := gzip.NewReader(archive)
+ if err != nil {
+ return err
+ }
+ tarReader := tar.NewReader(uncompressed)
+ for true {
+ header, err := tarReader.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ switch header.Typeflag {
+ case tar.TypeDir:
+ if err := fs.MkdirAll(header.Name, 0755); err != nil {
+ return err
+ }
+ case tar.TypeReg:
+ out, err := fs.Create(header.Name)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ if _, err := io.Copy(out, tarReader); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("Uknown type: %s", header.Name)
+ }
+ }
+ return nil
+}
+
+type fsAppRepository struct {
+ InMemoryAppRepository
+ fs billy.Filesystem
+}
+
+func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
+ all, err := fs.ReadDir(".")
+ if err != nil {
+ return nil, err
+ }
+ apps := make([]App, 0)
+ for _, e := range all {
+ if !e.IsDir() {
+ continue
+ }
+ appFS, err := fs.Chroot(e.Name())
+ if err != nil {
+ return nil, err
+ }
+ app, err := loadApp(appFS)
+ if err != nil {
+ log.Printf("Ignoring directory %s: %s", e.Name(), err)
+ continue
+ }
+ apps = append(apps, app)
+ }
+ return &fsAppRepository{
+ NewInMemoryAppRepository(apps),
+ fs,
+ }, nil
+}
+
+func loadApp(fs billy.Filesystem) (App, error) {
+ items, err := fs.ReadDir(".")
+ if err != nil {
+ return nil, err
+ }
+ var contents bytes.Buffer
+ for _, i := range items {
+ if i.IsDir() {
+ continue
+ }
+ f, err := fs.Open(i.Name())
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ if _, err := io.Copy(&contents, f); err != nil {
+ return nil, err
+ }
+ }
+ cfg, err := processCueConfig(contents.String())
+ if err != nil {
+ return nil, err
+ }
+ return NewCueEnvApp(cfg)
+}
+
+func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
+ contents, err := fs.ReadFile(f)
+ if err != nil {
+ return nil, err
+ }
+ return processCueConfig(string(contents))
+}
+
+func processCueConfig(contents string) (*cue.Value, error) {
+ ctx := cuecontext.New()
+ cfg := ctx.CompileString(contents + cueBaseConfig)
+ if err := cfg.Err(); err != nil {
+ return nil, err
+ }
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
+
+// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
+// schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
+// if err != nil {
+// panic(err)
+// }
+// return StoreApp{
+// App{
+// "maddy",
+// []string{"app-maddy"},
+// []*template.Template{
+// tmpls.Lookup("maddy.yaml"),
+// },
+// schema,
+// nil,
+// nil,
+// },
+// `<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>`,
+// "SMPT/IMAP server to communicate via email.",
+// }
+// }
+
+func FindEnvApp(r AppRepository, name string) (EnvApp, error) {
+ app, err := r.Find(name)
+ if err != nil {
+ return nil, err
+ }
+ if a, ok := app.(EnvApp); ok {
+ return a, nil
+ } else {
+ return nil, fmt.Errorf("not found")
+ }
+}
+
+func FindInfraApp(r AppRepository, name string) (InfraApp, error) {
+ app, err := r.Find(name)
+ if err != nil {
+ return nil, err
+ }
+ if a, ok := app.(InfraApp); ok {
+ return a, nil
+ } else {
+ return nil, fmt.Errorf("not found")
+ }
+}
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 4524a30..a979342 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -1,47 +1,41 @@
package installer
import (
+ "net"
"testing"
)
func TestAuthProxyEnabled(t *testing.T) {
r := NewInMemoryAppRepository(CreateAllApps())
for _, app := range []string{"rpuppy", "Pi-hole", "url-shortener"} {
- a, err := r.Find(app)
+ a, err := FindEnvApp(r, app)
if err != nil {
t.Fatal(err)
}
if a == nil {
t.Fatal("returned app is nil")
}
- d := Derived{
- Release: Release{
- Namespace: "foo",
- },
- Global: Values{
- PCloudEnvName: "dodo",
- Id: "id",
- ContactEmail: "foo@bar.ge",
- Domain: "bar.ge",
- PrivateDomain: "p.bar.ge",
- PublicIP: "1.2.3.4",
- NamespacePrefix: "id-",
- },
- Values: map[string]any{
- "network": map[string]any{
- "name": "Public",
- "ingressClass": "dodo-ingress-public",
- "certificateIssuer": "id-public",
- "domain": "bar.ge",
- },
- "subdomain": "woof",
- "auth": map[string]any{
- "enabled": true,
- "groups": "a,b",
- },
+ release := Release{
+ Namespace: "foo",
+ }
+ env := AppEnvConfig{
+ InfraName: "dodo",
+ Id: "id",
+ ContactEmail: "foo@bar.ge",
+ Domain: "bar.ge",
+ PrivateDomain: "p.bar.ge",
+ PublicIP: []net.IP{net.ParseIP("1.2.3.4")},
+ NamespacePrefix: "id-",
+ }
+ values := map[string]any{
+ "network": "Public",
+ "subdomain": "woof",
+ "auth": map[string]any{
+ "enabled": true,
+ "groups": "a,b",
},
}
- rendered, err := a.Render(d)
+ rendered, err := a.Render(release, env, values)
if err != nil {
t.Fatal(err)
}
@@ -54,40 +48,33 @@
func TestAuthProxyDisabled(t *testing.T) {
r := NewInMemoryAppRepository(CreateAllApps())
for _, app := range []string{"rpuppy", "Pi-hole", "url-shortener"} {
- a, err := r.Find(app)
+ a, err := FindEnvApp(r, app)
if err != nil {
t.Fatal(err)
}
if a == nil {
t.Fatal("returned app is nil")
}
- d := Derived{
- Release: Release{
- Namespace: "foo",
- },
- Global: Values{
- PCloudEnvName: "dodo",
- Id: "id",
- ContactEmail: "foo@bar.ge",
- Domain: "bar.ge",
- PrivateDomain: "p.bar.ge",
- PublicIP: "1.2.3.4",
- NamespacePrefix: "id-",
- },
- Values: map[string]any{
- "network": map[string]any{
- "name": "Public",
- "ingressClass": "dodo-ingress-public",
- "certificateIssuer": "id-public",
- "domain": "bar.ge",
- },
- "subdomain": "woof",
- "auth": map[string]any{
- "enabled": false,
- },
+ release := Release{
+ Namespace: "foo",
+ }
+ env := AppEnvConfig{
+ InfraName: "dodo",
+ Id: "id",
+ ContactEmail: "foo@bar.ge",
+ Domain: "bar.ge",
+ PrivateDomain: "p.bar.ge",
+ PublicIP: []net.IP{net.ParseIP("1.2.3.4")},
+ NamespacePrefix: "id-",
+ }
+ values := map[string]any{
+ "network": "Public",
+ "subdomain": "woof",
+ "auth": map[string]any{
+ "enabled": false,
},
}
- rendered, err := a.Render(d)
+ rendered, err := a.Render(release, env, values)
if err != nil {
t.Fatal(err)
}
@@ -99,29 +86,27 @@
func TestGroupMemberships(t *testing.T) {
r := NewInMemoryAppRepository(CreateAllApps())
- a, err := r.Find("memberships")
+ a, err := FindEnvApp(r, "memberships")
if err != nil {
t.Fatal(err)
}
if a == nil {
t.Fatal("returned app is nil")
}
- d := Derived{
- Release: Release{
- Namespace: "foo",
- },
- Global: Values{
- PCloudEnvName: "dodo",
- Id: "id",
- ContactEmail: "foo@bar.ge",
- Domain: "bar.ge",
- PrivateDomain: "p.bar.ge",
- PublicIP: "1.2.3.4",
- NamespacePrefix: "id-",
- },
- Values: map[string]any{},
+ release := Release{
+ Namespace: "foo",
}
- rendered, err := a.Render(d)
+ env := AppEnvConfig{
+ InfraName: "dodo",
+ Id: "id",
+ ContactEmail: "foo@bar.ge",
+ Domain: "bar.ge",
+ PrivateDomain: "p.bar.ge",
+ PublicIP: []net.IP{net.ParseIP("1.2.3.4")},
+ NamespacePrefix: "id-",
+ }
+ values := map[string]any{}
+ rendered, err := a.Render(release, env, values)
if err != nil {
t.Fatal(err)
}
@@ -132,42 +117,35 @@
func TestGerrit(t *testing.T) {
r := NewInMemoryAppRepository(CreateAllApps())
- a, err := r.Find("gerrit")
+ a, err := FindEnvApp(r, "gerrit")
if err != nil {
t.Fatal(err)
}
if a == nil {
t.Fatal("returned app is nil")
}
- d := Derived{
- Release: Release{
- Namespace: "foo",
- },
- Global: Values{
- PCloudEnvName: "dodo",
- Id: "id",
- ContactEmail: "foo@bar.ge",
- Domain: "bar.ge",
- PrivateDomain: "p.bar.ge",
- PublicIP: "1.2.3.4",
- NamespacePrefix: "id-",
- },
- Values: map[string]any{
- "subdomain": "gerrit",
- "network": map[string]any{
- "name": "Private",
- "ingressClass": "id-ingress-private",
- "domain": "p.bar.ge",
- "allocatePortAddr": "http://foo.bar/api/allocate",
- },
- "key": map[string]any{
- "public": "foo",
- "private": "bar",
- },
- "sshPort": 22,
- },
+ release := Release{
+ Namespace: "foo",
}
- rendered, err := a.Render(d)
+ env := AppEnvConfig{
+ InfraName: "dodo",
+ Id: "id",
+ ContactEmail: "foo@bar.ge",
+ Domain: "bar.ge",
+ PrivateDomain: "p.bar.ge",
+ PublicIP: []net.IP{net.ParseIP("1.2.3.4")},
+ NamespacePrefix: "id-",
+ }
+ values := map[string]any{
+ "subdomain": "gerrit",
+ "network": "Private",
+ "key": map[string]any{
+ "public": "foo",
+ "private": "bar",
+ },
+ "sshPort": 22,
+ }
+ rendered, err := a.Render(release, env, values)
if err != nil {
t.Fatal(err)
}
@@ -178,37 +156,30 @@
func TestJenkins(t *testing.T) {
r := NewInMemoryAppRepository(CreateAllApps())
- a, err := r.Find("jenkins")
+ a, err := FindEnvApp(r, "jenkins")
if err != nil {
t.Fatal(err)
}
if a == nil {
t.Fatal("returned app is nil")
}
- d := Derived{
- Release: Release{
- Namespace: "foo",
- },
- Global: Values{
- PCloudEnvName: "dodo",
- Id: "id",
- ContactEmail: "foo@bar.ge",
- Domain: "bar.ge",
- PrivateDomain: "p.bar.ge",
- PublicIP: "1.2.3.4",
- NamespacePrefix: "id-",
- },
- Values: map[string]any{
- "subdomain": "jenkins",
- "network": map[string]any{
- "name": "Private",
- "ingressClass": "id-ingress-private",
- "domain": "p.bar.ge",
- "allocatePortAddr": "http://foo.bar/api/allocate",
- },
- },
+ release := Release{
+ Namespace: "foo",
}
- rendered, err := a.Render(d)
+ env := AppEnvConfig{
+ InfraName: "dodo",
+ Id: "id",
+ ContactEmail: "foo@bar.ge",
+ Domain: "bar.ge",
+ PrivateDomain: "p.bar.ge",
+ PublicIP: []net.IP{net.ParseIP("1.2.3.4")},
+ NamespacePrefix: "id-",
+ }
+ values := map[string]any{
+ "subdomain": "jenkins",
+ "network": "Private",
+ }
+ rendered, err := a.Render(release, env, values)
if err != nil {
t.Fatal(err)
}
diff --git a/core/installer/bootstrapper.go b/core/installer/bootstrapper.go
index ad01140..6b65096 100644
--- a/core/installer/bootstrapper.go
+++ b/core/installer/bootstrapper.go
@@ -34,21 +34,33 @@
return Bootstrapper{cl, ns, ha, appRepo}
}
-func (b Bootstrapper) Run(env EnvConfig) error {
- if err := b.ns.Create(env.Name); err != nil {
+func (b Bootstrapper) findApp(name string) (InfraApp, error) {
+ app, err := b.appRepo.Find(name)
+ if err != nil {
+ return nil, err
+ }
+ if a, ok := app.(InfraApp); ok {
+ return a, nil
+ } else {
+ return nil, fmt.Errorf("not found")
+ }
+}
+
+func (b Bootstrapper) Run(env BootstrapConfig) error {
+ if err := b.ns.Create(env.InfraName); err != nil {
return err
}
if err := b.installMetallb(env); err != nil {
return err
}
- if err := b.installLonghorn(env.Name, env.StorageDir, env.VolumeDefaultReplicaCount); err != nil {
+ if err := b.installLonghorn(env.InfraName, env.StorageDir, env.VolumeDefaultReplicaCount); err != nil {
return err
}
bootstrapJobKeys, err := NewSSHKeyPair("bootstrapper")
if err != nil {
return err
}
- if err := b.installSoftServe(bootstrapJobKeys.AuthorizedKey(), env.Name, env.ServiceIPs.ConfigRepo); err != nil {
+ if err := b.installSoftServe(bootstrapJobKeys.AuthorizedKey(), env.InfraName, env.ServiceIPs.ConfigRepo); err != nil {
return err
}
time.Sleep(30 * time.Second)
@@ -67,7 +79,7 @@
if ss.AddPublicKey("admin", string(env.AdminPublicKey)); err != nil {
return err
}
- if err := b.installFluxcd(ss, env.Name); err != nil {
+ if err := b.installFluxcd(ss, env.InfraName); err != nil {
return err
}
fmt.Println("Fluxcd installed")
@@ -80,35 +92,43 @@
if err != nil {
return err
}
+ mgr, err := NewInfraAppManager(repoIO, b.ns)
+ if err != nil {
+ return err
+ }
fmt.Println("Configuring main repo")
if err := configureMainRepo(repoIO, env); err != nil {
return err
}
fmt.Println("Installing infrastructure services")
- if err := b.installInfrastructureServices(repoIO, env); err != nil {
+ if err := b.installInfrastructureServices(mgr, env); err != nil {
+ return err
+ }
+ fmt.Println("Installing public ingress")
+ if err := b.installIngressPublic(mgr, ss, env); err != nil {
return err
}
fmt.Println("Installing DNS Zone Manager")
- if err := b.installDNSZoneManager(repoIO, env); err != nil {
+ if err := b.installDNSZoneManager(mgr, env); err != nil {
return err
}
fmt.Println("Installing Fluxcd Reconciler")
- if err := b.installFluxcdReconciler(repoIO, ss, env); err != nil {
+ if err := b.installFluxcdReconciler(mgr, ss, env); err != nil {
return err
}
fmt.Println("Installing env manager")
- if err := b.installEnvManager(repoIO, ss, env); err != nil {
+ if err := b.installEnvManager(mgr, ss, env); err != nil {
return err
}
fmt.Println("Installing Ory Hydra Maester")
- if err := b.installOryHydraMaester(repoIO, env); err != nil {
+ if err := b.installOryHydraMaester(mgr, env); err != nil {
return err
}
fmt.Println("Environment ready to use")
return nil
}
-func (b Bootstrapper) installMetallb(env EnvConfig) error {
+func (b Bootstrapper) installMetallb(env BootstrapConfig) error {
if err := b.installMetallbNamespace(env); err != nil {
return err
}
@@ -127,9 +147,9 @@
return nil
}
-func (b Bootstrapper) installMetallbNamespace(env EnvConfig) error {
+func (b Bootstrapper) installMetallbNamespace(env BootstrapConfig) error {
fmt.Println("Installing metallb namespace")
- config, err := b.ha.New(env.Name)
+ config, err := b.ha.New(env.InfraName)
if err != nil {
return err
}
@@ -146,7 +166,7 @@
},
}
installer := action.NewInstall(config)
- installer.Namespace = env.Name
+ installer.Namespace = env.InfraName
installer.ReleaseName = "metallb-ns"
installer.Wait = true
installer.WaitForJobs = true
@@ -395,26 +415,21 @@
return nil
}
-func (b Bootstrapper) installInfrastructureServices(repo RepoIO, env EnvConfig) error {
+func (b Bootstrapper) installInfrastructureServices(mgr *InfraAppManager, env BootstrapConfig) error {
install := func(name string) error {
fmt.Printf("Installing infrastructure service %s\n", name)
- app, err := b.appRepo.Find(name)
+ app, err := b.findApp(name)
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s-%s", env.Name, app.Namespace())
- derived := Derived{
- Global: Values{
- PCloudEnvName: env.Name,
- },
- }
- return InstallApp(repo, b.ns, app, filepath.Join("/infrastructure", app.Name()), namespace, nil, derived)
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{})
}
appsToInstall := []string{
"resource-renderer-controller",
"headscale-controller",
"csi-driver-smb",
- "ingress-public",
"cert-manager",
}
for _, name := range appsToInstall {
@@ -425,9 +440,18 @@
return nil
}
-func configureMainRepo(repo RepoIO, env EnvConfig) error {
+func configureMainRepo(repo RepoIO, bootstrap BootstrapConfig) error {
return repo.Atomic(func(r RepoFS) (string, error) {
- if err := WriteYaml(r, "config.yaml", env); err != nil {
+ if err := WriteYaml(r, "bootstrap-config.yaml", bootstrap); err != nil {
+ return "", err
+ }
+ infra := InfraConfig{
+ Name: bootstrap.InfraName,
+ PublicIP: bootstrap.PublicIP,
+ InfraNamespacePrefix: bootstrap.NamespacePrefix,
+ InfraAdminPublicKey: bootstrap.AdminPublicKey,
+ }
+ if err := WriteYaml(r, "config.yaml", infra); err != nil {
return "", err
}
if err := WriteYaml(r, "env-cidrs.yaml", EnvCIDRs{}); err != nil {
@@ -435,7 +459,7 @@
}
kust := NewKustomization()
kust.AddResources(
- fmt.Sprintf("%s-flux", env.Name),
+ fmt.Sprintf("%s-flux", bootstrap.InfraName),
"infrastructure",
"environments",
)
@@ -459,7 +483,7 @@
url: https://github.com/giolekva/pcloud
ref:
branch: main
-`, env.Name)))
+`, bootstrap.InfraName)))
if err != nil {
return "", err
}
@@ -476,89 +500,94 @@
})
}
-func (b Bootstrapper) installEnvManager(repo RepoIO, ss *soft.Client, env EnvConfig) error {
+func (b Bootstrapper) installEnvManager(mgr *InfraAppManager, ss *soft.Client, env BootstrapConfig) error {
keys, err := NewSSHKeyPair("env-manager")
if err != nil {
return err
}
- user := fmt.Sprintf("%s-env-manager", env.Name)
+ user := fmt.Sprintf("%s-env-manager", env.InfraName)
if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
return err
}
if err := ss.MakeUserAdmin(user); err != nil {
return err
}
- app, err := b.appRepo.Find("env-manager")
+ app, err := b.findApp("env-manager")
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s-%s", env.Name, app.Namespace())
- derived := Derived{
- Global: Values{
- PCloudEnvName: env.Name,
- },
- Values: map[string]any{
- "repoIP": env.ServiceIPs.ConfigRepo,
- "repoPort": 22,
- "repoName": "config",
- "sshPrivateKey": string(keys.RawPrivateKey()),
- },
- }
- return InstallApp(repo, b.ns, app, filepath.Join("/infrastructure", app.Name()), namespace, derived.Values, derived)
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{
+ "repoIP": env.ServiceIPs.ConfigRepo,
+ "repoPort": 22,
+ "repoName": "config",
+ "sshPrivateKey": string(keys.RawPrivateKey()),
+ })
}
-func (b Bootstrapper) installOryHydraMaester(repo RepoIO, env EnvConfig) error {
- app, err := b.appRepo.Find("hydra-maester")
+func (b Bootstrapper) installIngressPublic(mgr *InfraAppManager, ss *soft.Client, env BootstrapConfig) error {
+ keys, err := NewSSHKeyPair("port-allocator")
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s-%s", env.Name, app.Namespace())
- derived := Derived{
- Global: Values{
- PCloudEnvName: env.Name,
- },
+ user := fmt.Sprintf("%s-port-allocator", env.InfraName)
+ if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
+ return err
}
- return InstallApp(repo, b.ns, app, filepath.Join("/infrastructure", app.Name()), namespace, nil, derived)
+ if err := ss.AddReadWriteCollaborator("config", user); err != nil {
+ return err
+ }
+ app, err := b.findApp("ingress-public")
+ if err != nil {
+ return err
+ }
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{
+ "sshPrivateKey": string(keys.RawPrivateKey()),
+ })
}
-func (b Bootstrapper) installDNSZoneManager(repo RepoIO, env EnvConfig) error {
+func (b Bootstrapper) installOryHydraMaester(mgr *InfraAppManager, env BootstrapConfig) error {
+ app, err := b.findApp("hydra-maester")
+ if err != nil {
+ return err
+ }
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{})
+}
+
+func (b Bootstrapper) installDNSZoneManager(mgr *InfraAppManager, env BootstrapConfig) error {
const (
volumeClaimName = "dns-zone-configs"
volumeMountPath = "/etc/pcloud/dns-zone-configs"
)
- app, err := b.appRepo.Find("dns-zone-manager")
+ app, err := b.findApp("dns-zone-manager")
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s-%s", env.Name, app.Namespace())
- derived := Derived{
- Global: Values{
- PCloudEnvName: env.Name,
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{
+ "volume": map[string]any{
+ "claimName": volumeClaimName,
+ "mountPath": volumeMountPath,
+ "size": "1Gi",
},
- Values: map[string]any{
- "volume": map[string]any{
- "claimName": volumeClaimName,
- "mountPath": volumeMountPath,
- "size": "1Gi",
- },
- "apiConfigMapName": dnsAPIConfigMapName,
- },
- }
- return InstallApp(repo, b.ns, app, filepath.Join("/infrastructure", app.Name()), namespace, derived.Values, derived)
+ "apiConfigMapName": dnsAPIConfigMapName,
+ })
}
-func (b Bootstrapper) installFluxcdReconciler(repo RepoIO, ss *soft.Client, env EnvConfig) error {
- app, err := b.appRepo.Find("fluxcd-reconciler")
+func (b Bootstrapper) installFluxcdReconciler(mgr *InfraAppManager, ss *soft.Client, env BootstrapConfig) error {
+ app, err := b.findApp("fluxcd-reconciler")
if err != nil {
return err
}
- namespace := fmt.Sprintf("%s-%s", env.Name, app.Namespace())
- derived := Derived{
- Global: Values{
- PCloudEnvName: env.Name,
- },
- }
- return InstallApp(repo, b.ns, app, filepath.Join("/infrastructure", app.Name()), namespace, nil, derived)
+ namespace := fmt.Sprintf("%s-%s", env.InfraName, app.Namespace())
+ appDir := filepath.Join("/infrastructure", app.Name())
+ return mgr.Install(app, appDir, namespace, map[string]any{})
}
type HelmActionConfigFactory interface {
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 621ad87..b6c083c 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -84,7 +84,7 @@
if err != nil {
return err
}
- config, err := m.Config()
+ env, err := m.Config()
if err != nil {
return err
}
@@ -110,7 +110,7 @@
r,
tasks.NewFluxcdReconciler( // TODO(gio): make reconciler address a flag
"http://fluxcd-reconciler.dodo-fluxcd-reconciler.svc.cluster.local",
- config.Values.Id,
+ env.Id,
),
)
return s.Start()
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index e6385b5..dc85470 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -3,8 +3,10 @@
import (
_ "embed"
"fmt"
+ "net"
"net/netip"
"os"
+ "strings"
"github.com/spf13/cobra"
@@ -34,7 +36,7 @@
"",
)
cmd.Flags().StringVar(
- &bootstrapFlags.envName,
+ &bootstrapFlags.publicIP,
"public-ip",
"",
"",
@@ -93,9 +95,10 @@
if err != nil {
return err
}
- envConfig := installer.EnvConfig{
- Name: bootstrapFlags.envName,
- PublicIP: bootstrapFlags.publicIP,
+ publicIPs, err := parseIPs(bootstrapFlags.publicIP)
+ envConfig := installer.BootstrapConfig{
+ InfraName: bootstrapFlags.envName,
+ PublicIP: publicIPs,
NamespacePrefix: fmt.Sprintf("%s-", bootstrapFlags.envName),
StorageDir: bootstrapFlags.storageDir,
VolumeDefaultReplicaCount: bootstrapFlags.volumeDefaultReplicaCount,
@@ -130,3 +133,15 @@
To: t,
}, nil
}
+
+func parseIPs(ip string) ([]net.IP, error) {
+ ret := make([]net.IP, 0)
+ for _, i := range strings.Split(ip, ",") {
+ ip := net.ParseIP(i)
+ if ip == nil {
+ return nil, fmt.Errorf("invalid ip: %s", i)
+ }
+ ret = append(ret, ip)
+ }
+ return ret, nil
+}
diff --git a/core/installer/config.go b/core/installer/config.go
index 5269512..99ae358 100644
--- a/core/installer/config.go
+++ b/core/installer/config.go
@@ -12,9 +12,9 @@
To netip.Addr `json:"to"`
}
-type EnvConfig struct {
- Name string `json:"name"`
- PublicIP string `json:"publicIP"`
+type BootstrapConfig struct {
+ InfraName string `json:"name"`
+ PublicIP []net.IP `json:"publicIP"`
NamespacePrefix string `json:"namespacePrefix"`
StorageDir string `json:"storageDir"`
VolumeDefaultReplicaCount int `json:"volumeDefaultReplicaCount"`
@@ -28,27 +28,3 @@
}
type EnvCIDRs []EnvCIDR
-
-type Config struct {
- Values Values `json:"input"` // TODO(gio): rename
-}
-
-type Values struct {
- PCloudEnvName string `json:"pcloudEnvName,omitempty"`
- Id string `json:"id,omitempty"`
- ContactEmail string `json:"contactEmail,omitempty"`
- Domain string `json:"domain,omitempty"`
- PrivateDomain string `json:"privateDomain,omitempty"`
- PublicIP string `json:"publicIP,omitempty"`
- NamespacePrefix string `json:"namespacePrefix,omitempty"`
- // GandiAPIToken string `json:"gandiAPIToken,omitempty"`
- // LighthouseAuthUIIP string `json:"lighthouseAuthUIIP,omitempty"`
- // LighthouseMainIP string `json:"lighthouseMainIP,omitempty"`
- // LighthouseMainPort string `json:"lighthouseMainPort,omitempty"`
- // MXHostname string `json:"mxHostname,omitempty"`
- // MailGatewayAddress string `json:"mailGatewayAddress,omitempty"`
- // MatrixOAuth2ClientSecret string `json:"matrixOAuth2ClientSecret,omitempty"`
- // MatrixStorageSize string `json:"matrixStorageSize,omitempty"`
- // PiholeOAuth2ClientSecret string `json:"piholeOAuth2ClientSecret,omitempty"`
- // PiholeOAuth2CookieSecret string `json:"piholeOAuth2CookieSecret,omitempty"`
-}
diff --git a/core/installer/derived.go b/core/installer/derived.go
index 813939e..baa453e 100644
--- a/core/installer/derived.go
+++ b/core/installer/derived.go
@@ -5,15 +5,10 @@
)
type Release struct {
- Namespace string `json:"namespace"`
- RepoAddr string `json:"repoAddr"`
- AppDir string `json:"appDir"`
-}
-
-type Derived struct {
- Release Release `json:"release"`
- Global Values `json:"global"`
- Values map[string]any `json:"input"` // TODO(gio): rename to input
+ AppInstanceId string `json:"appInstanceId"`
+ Namespace string `json:"namespace"`
+ RepoAddr string `json:"repoAddr"`
+ AppDir string `json:"appDir"`
}
type Network struct {
@@ -24,17 +19,19 @@
AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
}
-type AppConfig struct {
+type AppInstanceConfig struct {
Id string `json:"id"`
AppId string `json:"appId"`
- Config map[string]any `json:"config"`
- Derived Derived `json:"derived"`
+ Env AppEnvConfig `json:"env"`
+ Release Release `json:"release"`
+ Values map[string]any `json:"values"`
+ Input map[string]any `json:"input"`
}
-func (a AppConfig) Input(schema Schema) map[string]any {
- ret, err := derivedToConfig(a.Derived.Values, schema)
+func (a AppInstanceConfig) InputToValues(schema Schema) map[string]any {
+ ret, err := derivedToConfig(a.Input, schema)
if err != nil {
- panic(err) // TODO(gio): handle
+ panic(err)
}
return ret
}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index ffb4078..c5e80c6 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -102,10 +102,18 @@
if err != nil {
return nil
}
- return wt.Pull(&git.PullOptions{
+ err = wt.Pull(&git.PullOptions{
Auth: auth(r.signer),
Force: true,
})
+ if err == nil {
+ return nil
+ }
+ if errors.Is(err, git.NoErrAlreadyUpToDate) {
+ return nil
+ }
+ // TODO(gio): check `remote repository is empty`
+ return nil
}
func (r *repoIO) CommitAndPush(message string) error {
diff --git a/core/installer/tasks/env.go b/core/installer/tasks/env.go
index 7eaf0da..68fbe57 100644
--- a/core/installer/tasks/env.go
+++ b/core/installer/tasks/env.go
@@ -12,16 +12,17 @@
)
type state struct {
- infoListener EnvInfoListener
- publicIPs []net.IP
- nsCreator installer.NamespaceCreator
- repo installer.RepoIO
- ssAdminKeys *keygen.KeyPair
- ssClient *soft.Client
- fluxUserName string
- keys *keygen.KeyPair
- appManager *installer.AppManager
- appsRepo installer.AppRepository
+ infoListener EnvInfoListener
+ publicIPs []net.IP
+ nsCreator installer.NamespaceCreator
+ repo installer.RepoIO
+ ssAdminKeys *keygen.KeyPair
+ ssClient *soft.Client
+ fluxUserName string
+ keys *keygen.KeyPair
+ appManager *installer.AppManager
+ appsRepo installer.AppRepository
+ infraAppManager *installer.InfraAppManager
}
type Env struct {
@@ -46,13 +47,15 @@
startIP net.IP,
nsCreator installer.NamespaceCreator,
repo installer.RepoIO,
+ mgr *installer.InfraAppManager,
infoListener EnvInfoListener,
) (Task, DNSZoneRef) {
st := state{
- infoListener: infoListener,
- publicIPs: publicIPs,
- nsCreator: nsCreator,
- repo: repo,
+ infoListener: infoListener,
+ publicIPs: publicIPs,
+ nsCreator: nsCreator,
+ repo: repo,
+ infraAppManager: mgr,
}
t := newSequentialParentTask(
"Create env",
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index d718428..fb3047d 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -64,16 +64,14 @@
r.Atomic(func(r installer.RepoFS) (string, error) {
{
// TODO(giolekva): private domain can be configurable as well
- config := installer.Config{
- Values: installer.Values{
- PCloudEnvName: env.PCloudEnvName,
- Id: env.Name,
- ContactEmail: env.ContactEmail,
- Domain: env.Domain,
- PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
- PublicIP: st.publicIPs[0].String(),
- NamespacePrefix: fmt.Sprintf("%s-", env.Name),
- },
+ config := installer.AppEnvConfig{
+ Id: env.Name,
+ InfraName: env.PCloudEnvName,
+ Domain: env.Domain,
+ PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
+ ContactEmail: env.ContactEmail,
+ PublicIP: st.publicIPs,
+ NamespacePrefix: fmt.Sprintf("%s-", env.Name),
}
if err := installer.WriteYaml(r, "config.yaml", config); err != nil {
return "", err
@@ -94,7 +92,7 @@
interval: 1m0s
url: https://github.com/giolekva/pcloud
ref:
- branch: ingress-port-allocator
+ branch: main
`, env.Name)
if err != nil {
return "", err
@@ -166,14 +164,15 @@
{
ingressPrivateIP := startAddr
headscaleIP := ingressPrivateIP.Next()
- app, err := st.appsRepo.Find("metallb-ipaddresspool")
+ app, err := installer.FindEnvApp(st.appsRepo, "metallb-ipaddresspool")
if err != nil {
return err
}
{
- appDir := fmt.Sprintf("/apps/%s-ingress-private", app.Name())
+ instanceId := fmt.Sprintf("%s-ingress-private", app.Name())
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": fmt.Sprintf("%s-ingress-private", env.Name),
"from": ingressPrivateIP.String(),
"to": ingressPrivateIP.String(),
@@ -184,9 +183,10 @@
}
}
{
- appDir := fmt.Sprintf("/apps/%s-headscale", app.Name())
+ instanceId := fmt.Sprintf("%s-headscale", app.Name())
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": fmt.Sprintf("%s-headscale", env.Name),
"from": headscaleIP.String(),
"to": headscaleIP.String(),
@@ -197,9 +197,10 @@
}
}
{
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"name": env.Name,
"from": fromIP.String(),
"to": toIP.String(),
@@ -222,13 +223,14 @@
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
- app, err := st.appsRepo.Find("private-network")
+ app, err := installer.FindEnvApp(st.appsRepo, "private-network")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"privateNetwork": map[string]any{
"hostname": "private-network-proxy",
"username": "private-network-proxy",
@@ -246,25 +248,27 @@
func SetupCertificateIssuers(env Env, st *state) Task {
pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
- app, err := st.appsRepo.Find("certificate-issuer-public")
+ app, err := installer.FindEnvApp(st.appsRepo, "certificate-issuer-public")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{}); err != nil {
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
return err
}
return nil
})
priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
- app, err := st.appsRepo.Find("certificate-issuer-private")
+ app, err := installer.FindEnvApp(st.appsRepo, "certificate-issuer-private")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"apiConfigMap": map[string]any{
"name": "api-config", // TODO(gio): take from global pcloud config
"namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
@@ -279,13 +283,14 @@
func SetupAuth(env Env, st *state) Task {
t := newLeafTask("Setup", func() error {
- app, err := st.appsRepo.Find("core-auth")
+ app, err := installer.FindEnvApp(st.appsRepo, "core-auth")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
}); err != nil {
return err
@@ -302,13 +307,14 @@
func SetupGroupMemberships(env Env, st *state) Task {
t := newLeafTask("Setup", func() error {
- app, err := st.appsRepo.Find("memberships")
+ app, err := installer.FindEnvApp(st.appsRepo, "memberships")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"authGroups": strings.Join(initGroups, ","),
}); err != nil {
return err
@@ -325,13 +331,14 @@
func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
t := newLeafTask("Setup", func() error {
- app, err := st.appsRepo.Find("headscale")
+ app, err := installer.FindEnvApp(st.appsRepo, "headscale")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"subdomain": "headscale",
"ipSubnet": fmt.Sprintf("%s/24", startIP),
}); err != nil {
@@ -360,13 +367,14 @@
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
- app, err := st.appsRepo.Find("welcome")
+ app, err := installer.FindEnvApp(st.appsRepo, "welcome")
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
}); err != nil {
@@ -395,13 +403,14 @@
if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
return err
}
- app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
+ app, err := installer.FindEnvApp(st.appsRepo, "app-manager") // TODO(giolekva): configure
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s", app.Name())
+ instanceId := app.Name()
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
- if err := st.appManager.Install(app, appDir, namespace, map[string]any{
+ if err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"repoAddr": st.ssClient.GetRepoAddress("config"),
"sshPrivateKey": string(keys.RawPrivateKey()),
"authGroups": strings.Join(initGroups, ","),
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index 4ed7176..70b7939 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -38,31 +38,25 @@
func NewCreateConfigRepoTask(env Env, st *state) Task {
t := newLeafTask("Install Git server", func() error {
appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
- ssApp, err := appsRepo.Find("config-repo")
+ app, err := installer.FindInfraApp(appsRepo, "config-repo")
if err != nil {
return err
}
- ssAdminKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-admin-keys", env.Name))
+ adminKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-admin-keys", env.Name))
if err != nil {
return err
}
- st.ssAdminKeys = ssAdminKeys
- ssKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", env.Name))
+ st.ssAdminKeys = adminKeys
+ keys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", env.Name))
if err != nil {
return err
}
- derived := installer.Derived{
- Global: installer.Values{
- Id: env.Name,
- PCloudEnvName: env.PCloudEnvName,
- },
- Values: map[string]any{
- "privateKey": string(ssKeys.RawPrivateKey()),
- "publicKey": string(ssKeys.RawAuthorizedKey()),
- "adminKey": string(ssAdminKeys.RawAuthorizedKey()),
- },
- }
- return installer.InstallApp(st.repo, st.nsCreator, ssApp, filepath.Join("/environments", env.Name, "config-repo"), env.Name, derived.Values, derived)
+ appDir := filepath.Join("/environments", env.Name, "config-repo")
+ return st.infraAppManager.Install(app, appDir, env.Name, map[string]any{
+ "privateKey": string(keys.RawPrivateKey()),
+ "publicKey": string(keys.RawAuthorizedKey()),
+ "adminKey": string(adminKeys.RawAuthorizedKey()),
+ })
})
return &t
}
diff --git a/core/installer/values-tmpl/ingress-public.cue b/core/installer/values-tmpl/ingress-public.cue
index e723342..93ce90c 100644
--- a/core/installer/values-tmpl/ingress-public.cue
+++ b/core/installer/values-tmpl/ingress-public.cue
@@ -1,4 +1,10 @@
-input: {}
+import (
+ "encoding/base64"
+)
+
+input: {
+ sshPrivateKey: string
+}
name: "ingress-public"
namespace: "ingress-public"
@@ -11,6 +17,12 @@
tag: "v1.8.0"
pullPolicy: "IfNotPresent"
}
+ portAllocator: {
+ repository: "giolekva"
+ name: "port-allocator"
+ tag: "latest"
+ pullPolicy: "Always"
+ }
}
charts: {
@@ -22,6 +34,14 @@
namespace: global.pcloudEnvName
}
}
+ portAllocator: {
+ chart: "charts/port-allocator"
+ sourceRef: {
+ kind: "GitRepository"
+ name: "pcloud"
+ namespace: global.pcloudEnvName
+ }
+ }
}
helm: {
@@ -57,4 +77,17 @@
}
}
}
+ "port-allocator": {
+ chart: charts.portAllocator
+ values: {
+ repoAddr: release.repoAddr
+ sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+ ingressNginxPath: "\(release.appDir)/ingress-public.yaml"
+ image: {
+ repository: images.portAllocator.fullName
+ tag: images.portAllocator.tag
+ pullPolicy: images.portAllocator.pullPolicy
+ }
+ }
+ }
}
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index c1ae84a..8c25adf 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -75,7 +75,7 @@
<form id="config-form">
{{ if $instance }}
- {{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" false "Data" ($instance.Input $schema)) }}
+ {{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" false "Data" ($instance.InputToValues $schema)) }}
{{ else }}
{{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" false "Data" (dict)) }}
{{ end }}
@@ -93,7 +93,7 @@
{{ if or (not $instance) (ne $instance.Id .Id)}}
<details>
<summary>{{ .Id }}</summary>
- {{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" true "Data" (.Input $schema) ) }}
+ {{ template "schema-form" (dict "Schema" $schema "AvailableNetworks" $networks "ReadOnly" true "Data" (.InputToValues $schema)) }}
<a href="/instance/{{ .Id }}" role="button" class="secondary">View</a>
</details>
{{ end }}
@@ -139,7 +139,7 @@
<script>
let readme = "";
- let config = {{ if $instance }}JSON.parse({{ toJson ($instance.Input $schema) }}){{ else }}{}{{ end }};
+ let config = {{ if $instance }}JSON.parse({{ toJson ($instance.InputToValues $schema) }}){{ else }}{}{{ end }};
function setValue(name, value, config) {
let items = name.split(".")
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index b26a001..8a4bf34 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -66,11 +66,11 @@
}
type app struct {
- Name string `json:"name"`
- Icon template.HTML `json:"icon"`
- ShortDescription string `json:"shortDescription"`
- Slug string `json:"slug"`
- Instances []installer.AppConfig `json:"instances,omitempty"`
+ Name string `json:"name"`
+ Icon template.HTML `json:"icon"`
+ ShortDescription string `json:"shortDescription"`
+ Slug string `json:"slug"`
+ Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
}
func (s *AppManagerServer) handleAppRepo(c echo.Context) error {
@@ -95,25 +95,6 @@
if err != nil {
return err
}
- for _, instance := range instances {
- values, ok := instance.Config["Values"].(map[string]any)
- if !ok {
- return fmt.Errorf("Expected map")
- }
- for k, v := range values {
- if k == "Network" {
- n, ok := v.(map[string]any)
- if !ok {
- return fmt.Errorf("Expected map")
- }
- values["Network"], ok = n["Name"]
- if !ok {
- return fmt.Errorf("Missing Name")
- }
- break
- }
- }
- }
return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Name(), instances})
}
@@ -123,28 +104,11 @@
if err != nil {
return err
}
- values, ok := instance.Config["Values"].(map[string]any)
- if !ok {
- return fmt.Errorf("Expected map")
- }
- for k, v := range values {
- if k == "Network" {
- n, ok := v.(map[string]any)
- if !ok {
- return fmt.Errorf("Expected map")
- }
- values["Network"], ok = n["Name"]
- if !ok {
- return fmt.Errorf("Missing Name")
- }
- break
- }
- }
a, err := s.r.Find(instance.AppId)
if err != nil {
return err
}
- return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Name(), []installer.AppConfig{instance}})
+ return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Name(), []installer.AppInstanceConfig{instance}})
}
type file struct {
@@ -162,7 +126,7 @@
if err != nil {
return err
}
- global, err := s.m.Config()
+ env, err := s.m.Config()
if err != nil {
return err
}
@@ -170,22 +134,11 @@
if err := json.Unmarshal(contents, &values); err != nil {
return err
}
- if network, ok := values["network"]; ok {
- for _, n := range installer.CreateNetworks(global) {
- if n.Name == network { // TODO(giolekva): handle not found
- values["network"] = n
- }
- }
- }
- all := installer.Derived{
- Global: global.Values,
- Values: values,
- }
- a, err := s.r.Find(slug)
+ a, err := installer.FindEnvApp(s.r, slug)
if err != nil {
return err
}
- r, err := a.Render(all)
+ r, err := a.Render(installer.Release{}, env, values)
if err != nil {
return err
}
@@ -212,24 +165,25 @@
return err
}
log.Printf("Values: %+v\n", values)
- a, err := s.r.Find(slug)
+ a, err := installer.FindEnvApp(s.r, slug)
if err != nil {
return err
}
log.Printf("Found application: %s\n", slug)
- config, err := s.m.Config()
+ env, err := s.m.Config()
if err != nil {
return err
}
- log.Printf("Configuration: %+v\n", config)
+ log.Printf("Configuration: %+v\n", env)
suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
suffix, err := suffixGen.Generate()
if err != nil {
return err
}
- appDir := fmt.Sprintf("/apps/%s%s", a.Name(), suffix)
- namespace := fmt.Sprintf("%s%s%s", config.Values.NamespacePrefix, a.Namespace(), suffix)
- if err := s.m.Install(a, appDir, namespace, values); err != nil {
+ instanceId := a.Name() + suffix
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
+ namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
+ if err := s.m.Install(a, instanceId, appDir, namespace, values); err != nil {
log.Printf("%s\n", err.Error())
return err
}
@@ -252,7 +206,7 @@
if err := json.Unmarshal(contents, &values); err != nil {
return err
}
- a, err := s.r.Find(appConfig.AppId)
+ a, err := installer.FindEnvApp(s.r, appConfig.AppId)
if err != nil {
return err
}
@@ -291,9 +245,9 @@
}
type appContext struct {
- App installer.App
- Instance *installer.AppConfig
- Instances []installer.AppConfig
+ App installer.EnvApp
+ Instance *installer.AppInstanceConfig
+ Instances []installer.AppInstanceConfig
AvailableNetworks []installer.Network
}
@@ -311,7 +265,7 @@
return err
}
slug := c.Param("slug")
- a, err := s.r.Find(slug)
+ a, err := installer.FindEnvApp(s.r, slug)
if err != nil {
return err
}
@@ -346,7 +300,7 @@
if err != nil {
return err
}
- a, err := s.r.Find(instance.AppId)
+ a, err := installer.FindEnvApp(s.r, instance.AppId)
if err != nil {
return err
}
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 3efe330..219c67c 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -326,8 +326,13 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- var env installer.EnvConfig
- if err := installer.ReadYaml(s.repo, "config.yaml", &env); err != nil {
+ mgr, err := installer.NewInfraAppManager(s.repo, s.nsCreator)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ var infra installer.InfraConfig
+ if err := installer.ReadYaml(s.repo, "config.yaml", &infra); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -373,20 +378,18 @@
}
t, dns := tasks.NewCreateEnvTask(
tasks.Env{
- PCloudEnvName: env.Name,
+ PCloudEnvName: infra.Name,
Name: req.Name,
ContactEmail: req.ContactEmail,
Domain: req.Domain,
AdminPublicKey: req.AdminPublicKey,
NamespacePrefix: fmt.Sprintf("%s-", req.Name),
},
- []net.IP{
- net.ParseIP("135.181.48.180"),
- net.ParseIP("65.108.39.172"),
- },
+ infra.PublicIP,
startIP,
s.nsCreator,
s.repo,
+ mgr,
infoUpdater,
)
s.tasks[key] = t
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index 697fd07..a820586 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -210,21 +210,22 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- config, err := appManager.Config()
+ env, err := appManager.Config()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
{
- app, err := appsRepo.Find("headscale-user")
+ app, err := installer.FindEnvApp(appsRepo, "headscale-user")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- appDir := fmt.Sprintf("/apps/%s-%s", app.Name(), req.Username)
- namespace := fmt.Sprintf("%s%s", config.Values.NamespacePrefix, app.Namespace())
- if err := appManager.Install(app, appDir, namespace, map[string]any{
+ instanceId := fmt.Sprintf("%s-%s", app.Name(), req.Username)
+ appDir := fmt.Sprintf("/apps/%s", instanceId)
+ namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
+ if err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
"username": req.Username,
"preAuthKey": map[string]any{
"enabled": false,