installer: fully automate initial bootstrap and env creation
diff --git a/core/installer/Dockerfile b/core/installer/Dockerfile
new file mode 100644
index 0000000..0b3f04b
--- /dev/null
+++ b/core/installer/Dockerfile
@@ -0,0 +1,8 @@
+FROM alpine:latest
+
+ARG TARGETARCH
+
+COPY tmp/charts /charts
+
+COPY server_${TARGETARCH} /usr/bin/pcloud-installer
+RUN chmod +x /usr/bin/pcloud-installer
diff --git a/core/installer/Makefile b/core/installer/Makefile
index d80fa92..2a3ed5b 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -1,20 +1,41 @@
-image_arm64:
-	docker build --file=Dockerfile.flux --tag=giolekva/flux:latest . --platform=linux/arm64
+clean:
+	rm -rf tmp
+	rm -f server_*
+	rm -f pcloud
 
-push_arm64: image_arm64
+push_fluxcd_arm64:
+	docker build --file=Dockerfile.flux --tag=giolekva/flux:latest . --platform=linux/arm64
 	docker push giolekva/flux:latest
 
-build:
+build: clean
 	go build -o pcloud cmd/*.go
 
 bootstrap:
-	./pcloud bootstrap --kubeconfig=../../priv/kubeconfig --charts-dir=../../charts --admin-pub-key=/Users/lekva/.ssh/id_rsa.pub --admin-priv-key=/Users/lekva/.ssh/id_rsa --soft-serve-ip=192.168.0.211 --storage-dir=/pcloud-storage/longhorn
+	./pcloud bootstrap --kubeconfig=../../priv/kubeconfig --charts-dir=../../charts --admin-pub-key=/Users/lekva/.ssh/id_rsa.pub --soft-serve-ip=192.168.0.211 --storage-dir=/pcloud-storage/longhorn
 
 create_env:
-	./pcloud create-env --admin-priv-key=/Users/lekva/.ssh/id_rsa --name=lekva --ip=192.168.0.211
+	./pcloud create-env --admin-priv-key=/Users/lekva/.ssh/id_rsa --name=lekva --ip=192.168.0.211 --admin-username=gio
 
 rpuppy:
 	./pcloud install --ssh-key=/Users/lekva/.ssh/id_rsa --app=rpuppy --repo-addr=ssh://localhost:2222/lekva
 
 appmanager:
-	./pcloud appmanager --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://localhost:2222/lekva
+	./pcloud appmanager --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://192.168.0.211/lekva --port=9090
+
+
+
+
+## installer image
+build_arm64: export CGO_ENABLED=0
+build_arm64: export GO111MODULE=on
+build_arm64: export GOOS=linux
+build_arm64: export GOARCH=arm64
+build_arm64:
+	go build -o server_arm64 cmd/*.go
+
+push: clean build_arm64
+	mkdir tmp
+	cp -r ../../charts tmp/
+	podman build --tag=giolekva/pcloud-installer:latest .
+	rm -rf tmp
+	podman push giolekva/pcloud-installer:latest
diff --git a/core/installer/app.go b/core/installer/app.go
index fbbcd64..96caf68 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -5,6 +5,8 @@
 	"fmt"
 	"log"
 	"text/template"
+
+	"github.com/Masterminds/sprig/v3"
 )
 
 //go:embed values-tmpl
@@ -46,12 +48,11 @@
 }
 
 func CreateAllApps() []App {
-	tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*")
+	tmpls, err := template.New("root").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(valuesTmpls, "values-tmpl/*")
 	if err != nil {
 		log.Fatal(err)
 	}
 	return []App{
-		// CreateAppIngressPublic(tmpls),
 		CreateAppIngressPrivate(valuesTmpls, tmpls),
 		CreateAppCoreAuth(valuesTmpls, tmpls),
 		CreateAppVaultwarden(valuesTmpls, tmpls),
@@ -62,21 +63,16 @@
 		CreateAppJellyfin(valuesTmpls, tmpls),
 		CreateAppRpuppy(valuesTmpls, tmpls),
 		CreateAppHeadscale(valuesTmpls, tmpls),
-	}
-}
-
-func CreateAppIngressPublic(fs embed.FS, tmpls *template.Template) App {
-	schema, err := fs.ReadFile("values-tmpl/ingress-public.jsonschema")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"ingress-public",
-		[]*template.Template{
-			tmpls.Lookup("ingress-public.yaml"),
-		},
-		string(schema),
-		nil,
+		CreateAppTailscaleProxy(valuesTmpls, tmpls),
+		CreateMetallbConfigEnv(valuesTmpls, tmpls),
+		CreateEnvManager(valuesTmpls, tmpls),
+		CreateIngressPublic(valuesTmpls, tmpls),
+		CreateCertManager(valuesTmpls, tmpls),
+		CreateCertManagerWebhookGandi(valuesTmpls, tmpls),
+		CreateCertManagerWebhookGandiRole(valuesTmpls, tmpls),
+		CreateCSIDriverSMB(valuesTmpls, tmpls),
+		CreateResourceRendererController(valuesTmpls, tmpls),
+		CreateHeadscaleController(valuesTmpls, tmpls),
 	}
 }
 
@@ -98,6 +94,21 @@
 	}
 }
 
+func CreateCertificateIssuerPrivate(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/certificate-issuer-private.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"ingress-private",
+		[]*template.Template{
+			tmpls.Lookup("certificate-issuer-private.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("certificate-issuer-private.md"),
+	}
+}
+
 func CreateAppCoreAuth(fs embed.FS, tmpls *template.Template) App {
 	schema, err := fs.ReadFile("values-tmpl/core-auth.jsonschema")
 	if err != nil {
@@ -234,3 +245,153 @@
 		tmpls.Lookup("headscale.md"),
 	}
 }
+
+func CreateAppTailscaleProxy(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/tailscale-proxy.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"tailscale-proxy",
+		[]*template.Template{
+			tmpls.Lookup("tailscale-proxy.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("tailscale-proxy.md"),
+	}
+}
+
+func CreateMetallbConfigEnv(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/metallb-config-env.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"metallb-config-env",
+		[]*template.Template{
+			tmpls.Lookup("metallb-config-env.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("metallb-config-env.md"),
+	}
+}
+
+func CreateEnvManager(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/env-manager.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"env-manager",
+		[]*template.Template{
+			tmpls.Lookup("env-manager.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("env-manager.md"),
+	}
+}
+
+func CreateIngressPublic(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/ingress-public.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"ingress-public",
+		[]*template.Template{
+			tmpls.Lookup("ingress-public.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("ingress-public.md"),
+	}
+}
+
+func CreateCertManager(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/cert-manager.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"cert-manager",
+		[]*template.Template{
+			tmpls.Lookup("cert-manager.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("cert-manager.md"),
+	}
+}
+
+func CreateCertManagerWebhookGandi(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/cert-manager-webhook-gandi.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"cert-manager-webhook-gandi",
+		[]*template.Template{
+			tmpls.Lookup("cert-manager-webhook-gandi.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("cert-manager-webhook-gandi.md"),
+	}
+}
+
+func CreateCertManagerWebhookGandiRole(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/cert-manager-webhook-gandi-role.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"cert-manager-webhook-gandi-role",
+		[]*template.Template{
+			tmpls.Lookup("cert-manager-webhook-gandi-role.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("cert-manager-webhook-gandi-role.md"),
+	}
+}
+
+func CreateCSIDriverSMB(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/csi-driver-smb.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"csi-driver-smb",
+		[]*template.Template{
+			tmpls.Lookup("csi-driver-smb.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("csi-driver-smb.md"),
+	}
+}
+
+func CreateResourceRendererController(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/resource-renderer-controller.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"resource-renderer-controller",
+		[]*template.Template{
+			tmpls.Lookup("resource-renderer-controller.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("resource-renderer-controller.md"),
+	}
+}
+
+func CreateHeadscaleController(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/headscale-controller.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"headscale-controller",
+		[]*template.Template{
+			tmpls.Lookup("headscale-controller.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("headscale-controller.md"),
+	}
+}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 6bf0161..5c1822a 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -2,18 +2,8 @@
 
 import (
 	"fmt"
-	"io/fs"
 	"io/ioutil"
-	"net"
-	"time"
 
-	"golang.org/x/crypto/ssh"
-	"golang.org/x/exp/slices"
-
-	"github.com/go-git/go-billy/v5/util"
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"sigs.k8s.io/yaml"
 )
 
@@ -22,23 +12,17 @@
 const kustomizationFileName = "kustomization.yaml"
 
 type AppManager struct {
-	repo   *git.Repository
-	signer ssh.Signer
+	repoIO RepoIO
 }
 
-func NewAppManager(repo *git.Repository, signer ssh.Signer) (*AppManager, error) {
+func NewAppManager(repoIO RepoIO) (*AppManager, error) {
 	return &AppManager{
-		repo,
-		signer,
+		repoIO,
 	}, nil
 }
 
 func (m *AppManager) Config() (Config, error) {
-	wt, err := m.repo.Worktree()
-	if err != nil {
-		return Config{}, err
-	}
-	configF, err := wt.Filesystem.Open(configFileName)
+	configF, err := m.repoIO.Reader(configFileName)
 	if err != nil {
 		return Config{}, err
 	}
@@ -51,11 +35,7 @@
 }
 
 func (m *AppManager) AppConfig(name string) (map[string]any, error) {
-	wt, err := m.repo.Worktree()
-	if err != nil {
-		return nil, err
-	}
-	configF, err := wt.Filesystem.Open(wt.Filesystem.Join(appDirName, name, configFileName))
+	configF, err := m.repoIO.Reader(fmt.Sprintf("%s/%s/%s", appDirName, name, configFileName))
 	if err != nil {
 		return nil, err
 	}
@@ -70,17 +50,9 @@
 }
 
 func (m *AppManager) Install(app App, config map[string]any) error {
-	if err := m.repo.Fetch(&git.FetchOptions{
-		RemoteName: "origin",
-		Auth:       auth(m.signer),
-		Force:      true,
-	}); err != nil {
-		return err
-	}
-	wt, err := m.repo.Worktree()
-	if err != nil {
-		return err
-	}
+	// if err := m.repoIO.Fetch(); err != nil {
+	// 	return err
+	// }
 	globalConfig, err := m.Config()
 	if err != nil {
 		return err
@@ -89,101 +61,5 @@
 		"Global": globalConfig.Values,
 		"Values": config,
 	}
-	appsRoot, err := wt.Filesystem.Chroot(appDirName)
-	if err != nil {
-		return err
-	}
-	rootKustF, err := appsRoot.Open(kustomizationFileName)
-	if err != nil {
-		return err
-	}
-	defer rootKustF.Close()
-	rootKust, err := ReadKustomization(rootKustF)
-	if err != nil {
-		return err
-	}
-	appRoot, err := appsRoot.Chroot(app.Name)
-	if err != nil {
-		return err
-	}
-	if err := util.RemoveAll(appRoot, app.Name); err != nil {
-		return err
-	}
-	if err := appRoot.MkdirAll(app.Name, fs.ModePerm); err != nil {
-		return nil
-	}
-	appKust := NewKustomization()
-	for _, t := range app.Templates {
-		out, err := appRoot.Create(t.Name())
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		if err := t.Execute(out, all); err != nil {
-			return err
-		}
-		appKust.AddResources(t.Name())
-	}
-	{
-		out, err := appRoot.Create(configFileName)
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		configBytes, err := yaml.Marshal(config)
-		if err != nil {
-			return err
-		}
-		if _, err := out.Write(configBytes); err != nil {
-			return err
-		}
-	}
-	appKustF, err := appRoot.Create(kustomizationFileName)
-	if err != nil {
-		return err
-	}
-	defer appKustF.Close()
-	if err := appKust.Write(appKustF); err != nil {
-		return err
-	}
-	if !slices.Contains(rootKust.Resources, app.Name) {
-		rootKust.AddResources(app.Name)
-		rootKustFW, err := appsRoot.Create(kustomizationFileName)
-		if err != nil {
-			return err
-		}
-		defer rootKustFW.Close()
-		if err := rootKust.Write(rootKustFW); err != nil {
-			return err
-		}
-	}
-	// Commit and push
-	if err := wt.AddGlob("*"); err != nil {
-		return err
-	}
-	if _, err := wt.Commit(fmt.Sprintf("install: %s", app.Name), &git.CommitOptions{
-		Author: &object.Signature{
-			Name: "pcloud-appmanager",
-			When: time.Now(),
-		},
-	}); err != nil {
-		return err
-	}
-	return m.repo.Push(&git.PushOptions{
-		RemoteName: "origin",
-		Auth:       auth(m.signer),
-	})
-}
-
-func auth(signer ssh.Signer) *gitssh.PublicKeys {
-	return &gitssh.PublicKeys{
-		Signer: signer,
-		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
-			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
-				// TODO(giolekva): verify server public key
-				// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
-				return nil
-			},
-		},
-	}
+	return m.repoIO.InstallApp(app, "apps", all)
 }
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index b1e8135..9fd2045 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -63,7 +63,7 @@
 	if err != nil {
 		return err
 	}
-	m, err := installer.NewAppManager(repo, signer)
+	m, err := installer.NewAppManager(installer.NewRepoIO(repo, signer))
 	if err != nil {
 		return err
 	}
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
index baab5bf..9cfe955 100644
--- a/core/installer/cmd/apps.go
+++ b/core/installer/cmd/apps.go
@@ -60,10 +60,10 @@
 	if err != nil {
 		return err
 	}
-	m, err := installer.NewAppManager(
+	m, err := installer.NewAppManager(installer.NewRepoIO(
 		repo,
 		signer,
-	)
+	))
 	if err != nil {
 		return err
 	}
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index e5f3f2b..135b6b4 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -1,6 +1,3 @@
-// TODO
-// * ns pcloud not found
-
 package main
 
 import (
@@ -22,12 +19,12 @@
 )
 
 var bootstrapFlags struct {
+	pcloudEnvName             string
 	chartsDir                 string
 	adminPubKey               string
-	adminPrivKey              string
 	storageDir                string
 	volumeDefaultReplicaCount int
-	softServeIP               string
+	softServeIP               string // TODO(giolekva): reserve using metallb IPAddressPool
 }
 
 func bootstrapCmd() *cobra.Command {
@@ -36,6 +33,12 @@
 		RunE: bootstrapCmdRun,
 	}
 	cmd.Flags().StringVar(
+		&bootstrapFlags.pcloudEnvName,
+		"pcloud-env-name",
+		"pcloud",
+		"",
+	)
+	cmd.Flags().StringVar(
 		&bootstrapFlags.chartsDir,
 		"charts-dir",
 		"",
@@ -48,12 +51,6 @@
 		"",
 	)
 	cmd.Flags().StringVar(
-		&bootstrapFlags.adminPrivKey,
-		"admin-priv-key",
-		"",
-		"",
-	)
-	cmd.Flags().StringVar(
 		&bootstrapFlags.storageDir,
 		"storage-dir",
 		"",
@@ -75,73 +72,68 @@
 }
 
 func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
-	adminPubKey, adminPrivKey, err := readAdminKeys()
+	adminPubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
 	if err != nil {
 		return err
 	}
-	softServePub, softServePriv, err := installer.GenerateSSHKeys()
+	bootstrapJobKeys, err := installer.NewSSHKeyPair()
 	if err != nil {
 		return err
 	}
-	if err := installMetallbNamespace(); err != nil {
-		return err
-	}
 	if err := installMetallb(); err != nil {
 		return err
 	}
-	time.Sleep(1 * time.Minute)
-	if err := installMetallbConfig(); err != nil {
-		return err
-	}
 	if err := installLonghorn(); err != nil {
 		return err
 	}
-	time.Sleep(2 * time.Minute)
-	if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
+	time.Sleep(5 * time.Minute) // TODO(giolekva): implement proper wait
+	if err := installSoftServe(bootstrapJobKeys.Public); err != nil {
 		return err
 	}
-	time.Sleep(2 * time.Minute)
-	ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, adminPrivKey, log.Default())
+	time.Sleep(2 * time.Minute) // TODO(giolekva): implement proper wait
+	ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, []byte(bootstrapJobKeys.Private), log.Default())
 	if err != nil {
 		return err
 	}
-	fluxPub, fluxPriv, err := installer.GenerateSSHKeys()
+	if ss.AddPublicKey("admin", string(adminPubKey)); err != nil {
+		return err
+	}
+	if err := installFluxcd(ss, bootstrapFlags.pcloudEnvName); err != nil {
+		return err
+	}
+	repo, err := ss.GetRepo(bootstrapFlags.pcloudEnvName)
 	if err != nil {
 		return err
 	}
-	if err := ss.AddUser("flux", fluxPub); err != nil {
+	repoIO := installer.NewRepoIO(repo, ss.Signer)
+	if err := configurePCloudRepo(repoIO); err != nil {
 		return err
 	}
-	if err := ss.MakeUserAdmin("flux"); err != nil {
+	// TODO(giolekva): commit this to the repo above
+	global := map[string]any{
+		"PCloudEnvName": bootstrapFlags.pcloudEnvName,
+	}
+	if err := installInfrastructureServices(repoIO, global); err != nil {
 		return err
 	}
-	fmt.Println("Creating /pcloud repo")
-	if err := ss.AddRepository("pcloud", "# PCloud Systems"); err != nil {
+	if err := installEnvManager(ss, repoIO, global); err != nil {
 		return err
 	}
-	fmt.Println("Installing Flux")
-	if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
+	if ss.RemovePublicKey("admin", bootstrapJobKeys.Public); err != nil {
 		return err
 	}
-	pcloudRepo, err := ss.GetRepo("pcloud") // TODO(giolekva): configurable
-	if err != nil {
+
+	return nil
+}
+
+func installMetallb() error {
+	if err := installMetallbNamespace(); err != nil {
 		return err
 	}
-	if err := configurePCloudRepo(installer.NewRepoIO(pcloudRepo, ss.Signer)); err != nil {
+	if err := installMetallbService(); err != nil {
 		return err
 	}
-	// TODO(giolekva): everything below must be installed using Flux
-	if err := installIngressPublic(); err != nil {
-		return err
-	}
-	if err := installCertManager(); err != nil {
-		return err
-	}
-	if err := installCertManagerWebhookGandi(); err != nil {
-		return err
-	}
-	// TODO(giolekva): ideally should be installed automatically if any of the user installed apps requires it
-	if err := installSmbDriver(); err != nil {
+	if err := installMetallbConfig(); err != nil {
 		return err
 	}
 	return nil
@@ -150,7 +142,7 @@
 func installMetallbNamespace() error {
 	fmt.Println("Installing metallb namespace")
 	// config, err := createActionConfig("default")
-	config, err := createActionConfig("pcloud")
+	config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
 	if err != nil {
 		return err
 	}
@@ -168,7 +160,7 @@
 		},
 	}
 	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud"
+	installer.Namespace = bootstrapFlags.pcloudEnvName
 	installer.ReleaseName = "metallb-ns"
 	installer.Wait = true
 	installer.WaitForJobs = true
@@ -178,7 +170,7 @@
 	return nil
 }
 
-func installMetallb() error {
+func installMetallbService() error {
 	fmt.Println("Installing metallb")
 	// config, err := createActionConfig("default")
 	config, err := createActionConfig("metallb-system")
@@ -251,7 +243,7 @@
 
 func installLonghorn() error {
 	fmt.Println("Installing Longhorn")
-	config, err := createActionConfig("pcloud")
+	config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
 	if err != nil {
 		return err
 	}
@@ -288,9 +280,13 @@
 	return nil
 }
 
-func installSoftServe(pubKey, privKey, adminKey string) error {
+func installSoftServe(adminPublicKey string) error {
 	fmt.Println("Installing SoftServe")
-	config, err := createActionConfig("pcloud")
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
+		return err
+	}
+	config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
 	if err != nil {
 		return err
 	}
@@ -299,13 +295,13 @@
 		return err
 	}
 	values := map[string]interface{}{
-		"privateKey": privKey,
-		"publicKey":  pubKey,
-		"adminKey":   adminKey,
+		"privateKey": keys.Private,
+		"publicKey":  keys.Public,
+		"adminKey":   adminPublicKey,
 		"reservedIP": bootstrapFlags.softServeIP,
 	}
 	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud"
+	installer.Namespace = bootstrapFlags.pcloudEnvName
 	installer.CreateNamespace = true
 	installer.ReleaseName = "soft-serve"
 	installer.Wait = true
@@ -317,8 +313,39 @@
 	return nil
 }
 
-func installFlux(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
-	config, err := createActionConfig("pcloud")
+func installFluxcd(ss *soft.Client, pcloudEnvName string) error {
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
+		return err
+	}
+	if err := ss.AddUser("flux", keys.Public); err != nil {
+		return err
+	}
+	if err := ss.MakeUserAdmin("flux"); err != nil {
+		return err
+	}
+	fmt.Printf("Creating /%s repo", pcloudEnvName)
+	if err := ss.AddRepository(pcloudEnvName, "# PCloud Systems"); err != nil {
+		return err
+	}
+	fmt.Println("Installing Flux")
+	ssPublic, err := ss.GetPublicKey()
+	if err != nil {
+		return err
+	}
+	if err := installFluxBootstrap(
+		ss.GetRepoAddress(pcloudEnvName),
+		ss.IP,
+		string(ssPublic),
+		keys.Private,
+	); err != nil {
+		return err
+	}
+	return nil
+}
+
+func installFluxBootstrap(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
+	config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
 	if err != nil {
 		return err
 	}
@@ -333,7 +360,7 @@
 		"privateKey":              privateKey,
 	}
 	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud"
+	installer.Namespace = bootstrapFlags.pcloudEnvName
 	installer.CreateNamespace = true
 	installer.ReleaseName = "flux"
 	installer.Wait = true
@@ -345,150 +372,103 @@
 	return nil
 }
 
-func installIngressPublic() error {
-	config, err := createActionConfig("pcloud")
-	if err != nil {
-		return err
+func installInfrastructureServices(repo installer.RepoIO, global map[string]any) error {
+	values := map[string]any{
+		"Global": global,
 	}
-	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "ingress-nginx"))
-	if err != nil {
-		return err
+	appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	install := func(name string) error {
+		app, err := appRepo.Find(name)
+		if err != nil {
+			return err
+		}
+		return repo.InstallApp(*app, "infrastructure", values)
 	}
-	values := map[string]interface{}{
-		"fullnameOverride": "pcloud-ingress-public",
-		"controller": map[string]interface{}{
-			"service": map[string]interface{}{
-				"type": "LoadBalancer",
-			},
-			"ingressClassByName": true,
-			"ingressClassResource": map[string]interface{}{
-				"name":            "pcloud-ingress-public",
-				"enabled":         true,
-				"default":         false,
-				"controllerValue": "k8s.io/pcloud-ingress-public",
-			},
-			"config": map[string]interface{}{
-				"proxy-body-size": "100M",
-			},
-		},
-		"udp": map[string]interface{}{
-			"6881": "lekva-app-qbittorrent/torrent:6881",
-		},
-		"tcp": map[string]interface{}{
-			"6881": "lekva-app-qbittorrent/torrent:6881",
-		},
+	appsToInstall := []string{
+		"resource-renderer-controller",
+		"headscale-controller",
+		"csi-driver-smb",
+		"ingress-public",
+		"cert-manager",
+		"cert-manager-webhook-gandi",
+		"cert-manager-webhook-gandi-role",
 	}
-	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud-ingress-public"
-	installer.CreateNamespace = true
-	installer.ReleaseName = "ingress-public"
-	installer.Wait = true
-	installer.WaitForJobs = true
-	installer.Timeout = 20 * time.Minute
-	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
-		return err
-	}
-	return nil
-}
-
-func installCertManager() error {
-	config, err := createActionConfig("pcloud-cert-manager")
-	if err != nil {
-		return err
-	}
-	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "cert-manager"))
-	if err != nil {
-		return err
-	}
-	values := map[string]interface{}{
-		"fullnameOverride": "pcloud-cert-manager",
-		"installCRDs":      true,
-		"image": map[string]interface{}{
-			"tag":        "v1.11.1",
-			"pullPolicy": "IfNotPresent",
-		},
-	}
-	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud-cert-manager"
-	installer.CreateNamespace = true
-	installer.ReleaseName = "cert-manager"
-	installer.Wait = true
-	installer.WaitForJobs = true
-	installer.Timeout = 20 * time.Minute
-	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
-		return err
-	}
-	return nil
-}
-
-func installCertManagerWebhookGandi() error {
-	config, err := createActionConfig("pcloud-cert-manager")
-	if err != nil {
-		return err
-	}
-	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "cert-manager-webhook-gandi"))
-	if err != nil {
-		return err
-	}
-	values := map[string]interface{}{
-		"fullnameOverride": "pcloud-cert-manager-webhook-gandi",
-		"certManager": map[string]interface{}{
-			"namespace":          "pcloud-cert-manager",
-			"serviceAccountName": "pcloud-cert-manager",
-		},
-		"image": map[string]interface{}{
-			"repository": "giolekva/cert-manager-webhook-gandi",
-			"tag":        "v0.2.0",
-			"pullPolicy": "IfNotPresent",
-		},
-		"logLevel": 2,
-	}
-	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud-cert-manager"
-	installer.CreateNamespace = false
-	installer.ReleaseName = "cert-manager-webhook-gandi"
-	installer.Wait = true
-	installer.WaitForJobs = true
-	installer.Timeout = 20 * time.Minute
-	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
-		return err
-	}
-	return nil
-}
-
-func installSmbDriver() error {
-	config, err := createActionConfig("pcloud-csi-driver-smb")
-	if err != nil {
-		return err
-	}
-	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "csi-driver-smb"))
-	if err != nil {
-		return err
-	}
-	values := map[string]interface{}{}
-	installer := action.NewInstall(config)
-	installer.Namespace = "pcloud-csi-driver-smb"
-	installer.CreateNamespace = true
-	installer.ReleaseName = "csi-driver-smb"
-	installer.Wait = true
-	installer.WaitForJobs = true
-	installer.Timeout = 20 * time.Minute
-	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
-		return err
+	for _, name := range appsToInstall {
+		if err := install(name); err != nil {
+			return err
+		}
 	}
 	return nil
 }
 
 func configurePCloudRepo(repo installer.RepoIO) error {
-	kust := installer.NewKustomization()
-	kust.AddResources("pcloud-flux", "environments")
-	if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
+	{
+		kust := installer.NewKustomization()
+		kust.AddResources("pcloud-flux", "infrastructure", "environments")
+		if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
+			return err
+		}
+		{
+			out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
+			if err != nil {
+				return err
+			}
+			defer out.Close()
+			_, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: pcloud # TODO(giolekva): use more generic name
+  namespace: pcloud # TODO(giolekva): configurable
+spec:
+  interval: 1m0s
+  url: https://github.com/giolekva/pcloud
+  ref:
+    branch: main
+`))
+			if err != nil {
+				return err
+			}
+		}
+		infraKust := installer.NewKustomization()
+		infraKust.AddResources("pcloud-charts.yaml")
+		if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
+			return err
+		}
+		if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
+			return err
+		}
+		if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func installEnvManager(ss *soft.Client, repo installer.RepoIO, global map[string]any) error {
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
 		return err
 	}
-	if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
+	user := fmt.Sprintf("%s-env-manager", bootstrapFlags.pcloudEnvName)
+	if err := ss.AddUser(user, keys.Public); err != nil {
 		return err
 	}
-	return repo.CommitAndPush("initialize pcloud directory structure, environments with kustomization.yaml-s")
+	if err := ss.MakeUserAdmin(user); err != nil {
+		return err
+	}
+	appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	envManager, err := appRepo.Find("env-manager")
+	if err != nil {
+		return err
+	}
+	return repo.InstallApp(*envManager, "infrastructure", map[string]any{
+		"Global": global,
+		"Values": map[string]any{
+			"RepoIP":        bootstrapFlags.softServeIP,
+			"SSHPrivateKey": keys.Private,
+		},
+	})
 }
 
 func createActionConfig(namespace string) (*action.Configuration, error) {
@@ -506,15 +486,3 @@
 	}
 	return config, nil
 }
-
-func readAdminKeys() ([]byte, []byte, error) {
-	pubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
-	if err != nil {
-		return nil, nil, err
-	}
-	privKey, err := os.ReadFile(bootstrapFlags.adminPrivKey)
-	if err != nil {
-		return nil, nil, err
-	}
-	return pubKey, privKey, nil
-}
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
index 8b7dc1e..045b258 100644
--- a/core/installer/cmd/env.go
+++ b/core/installer/cmd/env.go
@@ -7,6 +7,7 @@
 	"embed"
 	"encoding/base64"
 	"fmt"
+	"github.com/spf13/cobra"
 	"log"
 	"os"
 	"path"
@@ -14,17 +15,17 @@
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
-	"github.com/spf13/cobra"
 )
 
 //go:embed env-tmpl
 var filesTmpls embed.FS
 
 var createEnvFlags struct {
-	name         string
-	ip           string
-	port         int
-	adminPrivKey string
+	name          string
+	ip            string
+	port          int
+	adminPrivKey  string
+	adminUsername string
 }
 
 func createEnvCmd() *cobra.Command {
@@ -56,6 +57,12 @@
 		"",
 		"",
 	)
+	cmd.Flags().StringVar(
+		&createEnvFlags.adminUsername,
+		"admin-username",
+		"",
+		"",
+	)
 	return cmd
 }
 
@@ -72,61 +79,190 @@
 	if err != nil {
 		return err
 	}
-	fmt.Println(string(ssPubKey))
-	pub, priv, err := installer.GenerateSSHKeys()
-	{
-		_ = priv
-	}
+	keys, err := installer.NewSSHKeyPair()
 	if err != nil {
 		return err
 	}
-	readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
-	if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
+	if 1 == 2 {
+		readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
+		if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
+			return err
+		}
+		fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
+		if err := ss.AddUser(fluxUserName, keys.Public); err != nil {
+			return err
+		}
+		if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
+			return err
+		}
+	}
+	envRepo, err := ss.GetRepo(createEnvFlags.name)
+	if envRepo == nil {
 		return err
 	}
-	fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
-	if err := ss.AddUser(fluxUserName, pub); err != nil {
+	if err := initEnvRepo(installer.NewRepoIO(envRepo, ss.Signer)); err != nil {
 		return err
 	}
-	if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
-		return err
-	}
-	repo, err := ss.GetRepo("pcloud")
-	if err != nil {
-		return err
-	}
-	repoIO := installer.NewRepoIO(repo, ss.Signer)
-	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
-	if err != nil {
-		return err
-	}
-	kust.AddResources(createEnvFlags.name)
-	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
-	if err != nil {
-		return err
-	}
-	for _, tmpl := range tmpls.Templates() {
-		dstPath := path.Join("environments", createEnvFlags.name, tmpl.Name())
-		dst, err := repoIO.Writer(dstPath)
+	if 1 == 2 {
+		repo, err := ss.GetRepo("pcloud")
 		if err != nil {
 			return err
 		}
-		defer dst.Close()
-		if err := tmpl.Execute(dst, map[string]string{
-			"Name":       createEnvFlags.name,
-			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(priv)),
-			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(pub)),
-			"GitHost":    createEnvFlags.ip,
-			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", createEnvFlags.ip, ssPubKey))),
+		repoIO := installer.NewRepoIO(repo, ss.Signer)
+		kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+		if err != nil {
+			return err
+		}
+		kust.AddResources(createEnvFlags.name)
+		tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+		if err != nil {
+			return err
+		}
+		for _, tmpl := range tmpls.Templates() {
+			dstPath := path.Join("environments", createEnvFlags.name, tmpl.Name())
+			dst, err := repoIO.Writer(dstPath)
+			if err != nil {
+				return err
+			}
+			defer dst.Close()
+			if err := tmpl.Execute(dst, map[string]string{
+				"Name":       createEnvFlags.name,
+				"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+				"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+				"GitHost":    createEnvFlags.ip,
+				"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", createEnvFlags.ip, ssPubKey))),
+			}); err != nil {
+				return err
+			}
+		}
+		if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+			return err
+		}
+		if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", createEnvFlags.name)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func initEnvRepo(r installer.RepoIO) error {
+	appManager, err := installer.NewAppManager(r)
+	if err != nil {
+		return err
+	}
+	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	if 1 == 2 {
+		config := installer.Config{ // TODO(gioleka): configurable
+			Values: installer.Values{
+				PCloudEnvName:   "pcloud",
+				Id:              "lekva",
+				ContactEmail:    "giolekva@gmail.com",
+				Domain:          "lekva.me",
+				PrivateDomain:   "p.lekva.me",
+				PublicIP:        "46.49.35.44",
+				NamespacePrefix: "lekva-",
+			},
+		}
+		if err := r.WriteYaml("config.yaml", config); err != nil {
+			return err
+		}
+		{
+			out, err := r.Writer("pcloud-charts.yaml")
+			if err != nil {
+				return err
+			}
+			defer out.Close()
+			_, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: pcloud
+  namespace: lekva
+spec:
+  interval: 1m0s
+  url: https://github.com/giolekva/pcloud
+  ref:
+    branch: main
+`))
+			if err != nil {
+				return err
+			}
+		}
+		rootKust := installer.NewKustomization()
+		rootKust.AddResources("pcloud-charts.yaml", "apps")
+		if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+			return err
+		}
+		appsKust := installer.NewKustomization()
+		if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+			return err
+		}
+		r.CommitAndPush("initialize config")
+		{
+			app, err := appsRepo.Find("metallb-config-env")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"IngressPrivate": "10.1.0.1",
+				"Headscale":      "10.1.0.2",
+				"SoftServe":      "10.1.0.3",
+				"Rest": map[string]any{
+					"From": "10.1.0.100",
+					"To":   "10.1.0.255",
+				},
+			}); err != nil {
+				return err
+			}
+		}
+		{
+			app, err := appsRepo.Find("ingress-private")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"GandiAPIToken": "", // TODO(gioleka): configurable
+			}); err != nil {
+				return err
+			}
+		}
+		{
+			app, err := appsRepo.Find("core-auth")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+			}); err != nil {
+				return err
+			}
+		}
+	}
+	{
+		app, err := appsRepo.Find("headscale")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "headscale",
 		}); err != nil {
 			return err
 		}
 	}
-	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
-		return err
-	}
-	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", createEnvFlags.name)); err != nil {
-		return err
+	if 1 == 2 {
+		{
+			app, err := appsRepo.Find("tailscale-proxy")
+			if err != nil {
+				return err
+			}
+			if err := appManager.Install(*app, map[string]any{
+				"Username": createEnvFlags.adminUsername,
+				"IPSubnet": "10.1.0.0/24",
+			}); err != nil {
+				return err
+			}
+			// TODO(giolekva): headscale accept routes
+		}
 	}
 	return nil
 }
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
new file mode 100644
index 0000000..4279445
--- /dev/null
+++ b/core/installer/cmd/env_manager.go
@@ -0,0 +1,323 @@
+package main
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"path"
+	"text/template"
+
+	"github.com/labstack/echo/v4"
+	"github.com/spf13/cobra"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/soft"
+)
+
+var envManagerFlags struct {
+	repoIP   string
+	repoPort int
+	sshKey   string
+	port     int
+}
+
+func envManagerCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "envmanager",
+		RunE: envManagerCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&envManagerFlags.repoIP,
+		"repo-ip",
+		"",
+		"",
+	)
+	cmd.Flags().IntVar(
+		&envManagerFlags.repoPort,
+		"repo-port",
+		22,
+		"",
+	)
+	cmd.Flags().StringVar(
+		&envManagerFlags.sshKey,
+		"ssh-key",
+		"",
+		"",
+	)
+	cmd.Flags().IntVar(
+		&envManagerFlags.port,
+		"port",
+		8080,
+		"",
+	)
+	return cmd
+}
+
+func envManagerCmdRun(cmd *cobra.Command, args []string) error {
+	sshKey, err := os.ReadFile(envManagerFlags.sshKey)
+	if err != nil {
+		return err
+	}
+	fmt.Println(string(sshKey))
+	ss, err := soft.NewClient(envManagerFlags.repoIP, envManagerFlags.repoPort, sshKey, log.Default())
+	if err != nil {
+		return err
+	}
+	b, err := ss.GetPublicKey()
+	if err != nil {
+		return err
+	}
+	fmt.Println(string(b))
+	fmt.Println(111)
+	repo, err := ss.GetRepo("pcloud")
+	fmt.Println(222)
+	if err != nil {
+		return err
+	}
+	fmt.Println(333)
+	repoIO := installer.NewRepoIO(repo, ss.Signer)
+	s := &envServer{
+		port: envManagerFlags.port,
+		ss:   ss,
+		repo: repoIO,
+	}
+	s.start()
+	return nil
+}
+
+type envServer struct {
+	port int
+	ss   *soft.Client
+	repo installer.RepoIO
+}
+
+func (s *envServer) start() {
+	e := echo.New()
+	e.POST("/env", s.createEnv)
+	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+type createEnvReq struct {
+	Name          string `json:"name"`
+	ContactEmail  string `json:"contactEmail"`
+	Domain        string `json:"domain"`
+	GandiAPIToken string `json:"gandiAPIToken"`
+	AdminUsername string `json:"adminUsername"`
+	// TODO(giolekva): take admin password as well
+}
+
+func (s *envServer) createEnv(c echo.Context) error {
+	var req createEnvReq
+	if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
+		return err
+	}
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
+		return err
+	}
+	{
+		readme := fmt.Sprintf("# %s PCloud environment", req.Name)
+		if err := s.ss.AddRepository(req.Name, readme); err != nil {
+			return err
+		}
+		fluxUserName := fmt.Sprintf("flux-%s", req.Name)
+		if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
+			return err
+		}
+		if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo(req.Name)
+		if repo == nil {
+			return err
+		}
+		if err := initNewEnv(installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo("pcloud")
+		if err != nil {
+			return err
+		}
+		ssPubKey, err := s.ss.GetPublicKey()
+		if err != nil {
+			return err
+		}
+		if err := addNewEnv(
+			installer.NewRepoIO(repo, s.ss.Signer),
+			req,
+			keys,
+			ssPubKey,
+		); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func initNewEnv(r installer.RepoIO, req createEnvReq) error {
+	appManager, err := installer.NewAppManager(r)
+	if err != nil {
+		return err
+	}
+	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	// TODO(giolekva): env name and ip should come from pcloud repo config.yaml
+	// TODO(giolekva): private domain can be configurable as well
+	config := installer.Config{
+		Values: installer.Values{
+			PCloudEnvName:   "pcloud",
+			Id:              req.Name,
+			ContactEmail:    req.ContactEmail,
+			Domain:          req.Domain,
+			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
+			PublicIP:        "46.49.35.44",
+			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
+		},
+	}
+	if err := r.WriteYaml("config.yaml", config); err != nil {
+		return err
+	}
+	{
+		out, err := r.Writer("pcloud-charts.yaml")
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		_, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: pcloud
+  namespace: lekva
+spec:
+  interval: 1m0s
+  url: https://github.com/giolekva/pcloud
+  ref:
+    branch: main
+`))
+		if err != nil {
+			return err
+		}
+	}
+	rootKust := installer.NewKustomization()
+	rootKust.AddResources("pcloud-charts.yaml", "apps")
+	if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+		return err
+	}
+	appsKust := installer.NewKustomization()
+	if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+		return err
+	}
+	r.CommitAndPush("initialize config")
+	{
+		app, err := appsRepo.Find("metallb-config-env")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"IngressPrivate": "10.1.0.1",
+			"Headscale":      "10.1.0.2",
+			"SoftServe":      "10.1.0.3",
+			"Rest": map[string]any{
+				"From": "10.1.0.100",
+				"To":   "10.1.0.255",
+			},
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("ingress-private")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"GandiAPIToken": req.GandiAPIToken,
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("core-auth")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("headscale")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "headscale",
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("tailscale-proxy")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Username": req.AdminUsername,
+			"IPSubnet": "10.1.0.0/24",
+		}); err != nil {
+			return err
+		}
+		// TODO(giolekva): headscale accept routes
+	}
+
+	return nil
+}
+
+func addNewEnv(
+	repoIO installer.RepoIO,
+	req createEnvReq,
+	keys installer.KeyPair,
+	pcloudRepoPublicKey []byte,
+) error {
+	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+	if err != nil {
+		return err
+	}
+	kust.AddResources(req.Name)
+	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+	if err != nil {
+		return err
+	}
+	for _, tmpl := range tmpls.Templates() {
+		dstPath := path.Join("environments", req.Name, tmpl.Name())
+		dst, err := repoIO.Writer(dstPath)
+		if err != nil {
+			return err
+		}
+		defer dst.Close()
+		if err := tmpl.Execute(dst, map[string]string{
+			"Name":       req.Name,
+			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+			"GitHost":    envManagerFlags.repoIP,
+			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", envManagerFlags.repoIP, pcloudRepoPublicKey))),
+		}); err != nil {
+			return err
+		}
+	}
+	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+		return err
+	}
+	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
index 7a71b90..e82b338 100644
--- a/core/installer/cmd/main.go
+++ b/core/installer/cmd/main.go
@@ -26,6 +26,7 @@
 	rootCmd.AddCommand(createEnvCmd())
 	rootCmd.AddCommand(installCmd())
 	rootCmd.AddCommand(appManagerCmd())
+	rootCmd.AddCommand(envManagerCmd())
 }
 
 func main() {
diff --git a/core/installer/config.go b/core/installer/config.go
index f234d04..6712894 100644
--- a/core/installer/config.go
+++ b/core/installer/config.go
@@ -12,23 +12,23 @@
 }
 
 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"`
-	GandiAPIToken            string `json:"gandiAPIToken,omitempty"`
-	NamespacePrefix          string `json:"namespacePrefix,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"`
+	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"`
 }
 
 func ReadConfig(r io.Reader) (Config, error) {
diff --git a/core/installer/go.mod b/core/installer/go.mod
index ac66c3a..f61dffd 100644
--- a/core/installer/go.mod
+++ b/core/installer/go.mod
@@ -3,12 +3,13 @@
 go 1.18
 
 require (
-	github.com/go-git/go-billy/v5 v5.3.1
-	github.com/go-git/go-git/v5 v5.4.2
+	github.com/Masterminds/sprig/v3 v3.2.2
+	github.com/go-git/go-billy/v5 v5.4.1
+	github.com/go-git/go-git/v5 v5.7.0
 	github.com/labstack/echo/v4 v4.10.2
 	github.com/spf13/cobra v1.4.0
-	golang.org/x/crypto v0.6.0
-	golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
+	golang.org/x/crypto v0.9.0
+	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	helm.sh/helm/v3 v3.9.0
 	sigs.k8s.io/yaml v1.3.0
 )
@@ -19,17 +20,17 @@
 	github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.1.1 // indirect
-	github.com/Masterminds/sprig/v3 v3.2.2 // indirect
 	github.com/Masterminds/squirrel v1.5.2 // indirect
-	github.com/Microsoft/go-winio v0.5.1 // indirect
-	github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
+	github.com/Microsoft/go-winio v0.5.2 // indirect
+	github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
-	github.com/acomagu/bufpipe v1.0.3 // indirect
+	github.com/acomagu/bufpipe v1.0.4 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
+	github.com/cloudflare/circl v1.3.3 // indirect
 	github.com/containerd/containerd v1.6.3 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -41,12 +42,12 @@
 	github.com/docker/go-metrics v0.0.1 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/emicklei/go-restful v2.9.5+incompatible // indirect
-	github.com/emirpasic/gods v1.12.0 // indirect
+	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/go-errors/errors v1.0.1 // indirect
-	github.com/go-git/gcfg v1.5.0 // indirect
+	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/go-logr/logr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -54,10 +55,11 @@
 	github.com/go-openapi/swag v0.19.14 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/btree v1.0.1 // indirect
 	github.com/google/gnostic v0.5.7-v3refs // indirect
-	github.com/google/go-cmp v0.5.8 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.2.0 // indirect
@@ -65,13 +67,13 @@
 	github.com/gosuri/uitable v0.0.4 // indirect
 	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/imdario/mergo v0.3.15 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jmoiron/sqlx v1.3.4 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
+	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/klauspost/compress v1.13.6 // indirect
 	github.com/labstack/gommon v0.4.0 // indirect
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
@@ -84,7 +86,6 @@
 	github.com/mattn/go-runewidth v0.0.9 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
-	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/locker v1.0.1 // indirect
@@ -98,6 +99,7 @@
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_golang v1.12.1 // indirect
@@ -109,23 +111,24 @@
 	github.com/sergi/go-diff v1.1.0 // indirect
 	github.com/shopspring/decimal v1.2.0 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
+	github.com/skeema/knownhosts v1.1.1 // indirect
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stretchr/testify v1.8.1 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
-	github.com/xanzy/ssh-agent v0.3.0 // indirect
+	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 	github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
-	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/net v0.10.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/term v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
+	golang.org/x/term v0.8.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
diff --git a/core/installer/go.sum b/core/installer/go.sum
index 7562e09..65ec121 100644
--- a/core/installer/go.sum
+++ b/core/installer/go.sum
@@ -64,30 +64,27 @@
 github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
 github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE=
 github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
-github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
-github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/hcsshim v0.9.2 h1:wB06W5aYFfUB3IvootYAY2WnOmIdgPGfqSI6tufQNnY=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
-github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
+github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
+github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
 github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
-github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
-github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
+github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
+github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -112,6 +109,7 @@
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
 github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
@@ -125,6 +123,9 @@
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
+github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -180,13 +181,13 @@
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
-github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
-github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -207,7 +208,6 @@
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -217,19 +217,16 @@
 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
-github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
 github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
-github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
-github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
-github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
-github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
-github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
-github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
-github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
+github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
+github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
+github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -331,8 +328,8 @@
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -403,13 +400,12 @@
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
+github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
 github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -430,8 +426,8 @@
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
-github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -509,7 +505,6 @@
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
@@ -572,6 +567,8 @@
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -632,12 +629,13 @@
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
+github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@@ -687,8 +685,8 @@
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
-github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -705,6 +703,7 @@
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
 github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
 github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
@@ -754,7 +753,6 @@
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -765,12 +763,12 @@
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -781,8 +779,8 @@
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
-golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -809,6 +807,8 @@
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -851,7 +851,6 @@
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -859,8 +858,11 @@
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -886,8 +888,10 @@
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -906,7 +910,6 @@
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -944,11 +947,9 @@
 golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -957,18 +958,27 @@
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -978,8 +988,10 @@
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1046,6 +1058,8 @@
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/core/installer/keys.go b/core/installer/keys.go
index bb39c4d..695176f 100644
--- a/core/installer/keys.go
+++ b/core/installer/keys.go
@@ -9,14 +9,19 @@
 	"golang.org/x/crypto/ssh"
 )
 
-func GenerateSSHKeys() (string, string, error) {
+type KeyPair struct {
+	Public  string
+	Private string
+}
+
+func NewSSHKeyPair() (KeyPair, error) {
 	pub, priv, err := ed25519.GenerateKey(rand.Reader)
 	if err != nil {
-		return "", "", err
+		return KeyPair{}, err
 	}
 	privEnc, err := x509.MarshalPKCS8PrivateKey(priv)
 	if err != nil {
-		return "", "", err
+		return KeyPair{}, err
 	}
 	privPem := pem.EncodeToMemory(
 		&pem.Block{
@@ -26,7 +31,10 @@
 	)
 	pubKey, err := ssh.NewPublicKey(pub)
 	if err != nil {
-		return "", "", err
+		return KeyPair{}, err
 	}
-	return string(ssh.MarshalAuthorizedKey(pubKey)), string(privPem), nil
+	return KeyPair{
+		Public:  string(ssh.MarshalAuthorizedKey(pubKey)),
+		Private: string(privPem),
+	}, nil
 }
diff --git a/core/installer/kustomization.go b/core/installer/kustomization.go
index f5f42e5..93f806a 100644
--- a/core/installer/kustomization.go
+++ b/core/installer/kustomization.go
@@ -2,6 +2,7 @@
 
 import (
 	"bytes"
+	"golang.org/x/exp/slices"
 	"io"
 	"io/ioutil"
 
@@ -46,5 +47,9 @@
 }
 
 func (k *Kustomization) AddResources(names ...string) {
-	k.Resources = append(k.Resources, names...)
+	for _, name := range names {
+		if !slices.Contains(k.Resources, name) {
+			k.Resources = append(k.Resources, name)
+		}
+	}
 }
diff --git a/core/installer/manifest.yaml b/core/installer/manifest.yaml
new file mode 100644
index 0000000..61f8d80
--- /dev/null
+++ b/core/installer/manifest.yaml
@@ -0,0 +1,75 @@
+# TODO(giolekva): finish
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: pcloud
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: pcloud-bootstrap
+  namespace: pcloud
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: pcloud-bootstrap
+  namespace: pcloud
+rules:
+- apiGroups:
+  - *
+  resources:
+  - *
+  verbs:
+  - *
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: pcloud:pcloud-bootstrap
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: pcloud:pcloud-bootstrap
+subjects:
+- kind: ServiceAccount
+  name: pcloud-bootstrap
+  namespace: pcloud
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: admin-pub-key
+  namespace: pcloud
+data: # TODO(giolekva): can it work without ssh-ed25519 prefix
+  key.pub: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOa7FUrmXzdY3no8qNGUk7OPaRcIUi8G7MVbLlff9eB/
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: pcloud-bootstrap
+  namespace: pcloud
+spec:
+  template:
+    spec:
+      serviceAccountName: pcloud-bootstrap
+      volumes:
+      - name: admin-pub-key
+        configMap:
+          name: admin-pub-key
+      containers:
+      - name: pcloud-bootstrap
+        image: giolekva/pcloud-installer:latest
+        imagePullPolicy: Always
+        volumeMounts:
+        - name: admin-pub-key
+          mountPath: /admin-pub-key
+        command:
+        - pcloud-installer
+        - bootstrap
+        - --pcloud-env-name=pcloud
+        - --admin-pub-key=/admin-pub-key/key.pub
+        - --soft-serve-ip=192.168.0.211
+        - --charts-dir=/charts
+        - --storage-dir=/pcloud-storage/longhorn
+      restartPolicy: Never
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index a1d86d2..97bf9b6 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -1,21 +1,34 @@
 package installer
 
 import (
+	"errors"
+	"fmt"
 	"io"
 	"io/fs"
+	"net"
+	"path"
 	"path/filepath"
 	"time"
 
+	"github.com/go-git/go-billy/v5/util"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
+	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"golang.org/x/crypto/ssh"
+	"sigs.k8s.io/yaml"
 )
 
 type RepoIO interface {
+	Fetch() error
 	ReadKustomization(path string) (*Kustomization, error)
 	WriteKustomization(path string, kust Kustomization) error
+	WriteYaml(path string, data any) error
 	CommitAndPush(message string) error
+	Reader(path string) (io.ReadCloser, error)
 	Writer(path string) (io.WriteCloser, error)
+	CreateDir(path string) error
+	RemoveDir(path string) error
+	InstallApp(app App, path string, values map[string]any) error
 }
 
 type repoIO struct {
@@ -30,12 +43,20 @@
 	}
 }
 
-func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
-	wt, err := r.repo.Worktree()
-	if err != nil {
-		return nil, err
+func (r *repoIO) Fetch() error {
+	err := r.repo.Fetch(&git.FetchOptions{
+		RemoteName: "origin",
+		Auth:       auth(r.signer),
+		Force:      true,
+	})
+	if err == nil || err == git.NoErrAlreadyUpToDate {
+		return nil
 	}
-	inp, err := wt.Filesystem.Open(path)
+	return err
+}
+
+func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
+	inp, err := r.Reader(path)
 	if err != nil {
 		return nil, err
 	}
@@ -43,6 +64,14 @@
 	return ReadKustomization(inp)
 }
 
+func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return nil, err
+	}
+	return wt.Filesystem.Open(path)
+}
+
 func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
 	wt, err := r.repo.Worktree()
 	if err != nil {
@@ -62,6 +91,21 @@
 	return kust.Write(out)
 }
 
+func (r *repoIO) WriteYaml(path string, data any) error {
+	out, err := r.Writer(path)
+	if err != nil {
+		return err
+	}
+	serialized, err := yaml.Marshal(data)
+	if err != nil {
+		return err
+	}
+	if _, err := out.Write(serialized); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (r *repoIO) CommitAndPush(message string) error {
 	wt, err := r.repo.Worktree()
 	if err != nil {
@@ -83,3 +127,80 @@
 		Auth:       auth(r.signer),
 	})
 }
+
+func (r *repoIO) CreateDir(path string) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	return wt.Filesystem.MkdirAll(path, fs.ModePerm)
+}
+
+func (r *repoIO) RemoveDir(path string) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	err = util.RemoveAll(wt.Filesystem, path)
+	if err == nil || errors.Is(err, fs.ErrNotExist) {
+		return nil
+	}
+	return err
+}
+
+func (r *repoIO) InstallApp(app App, root string, values map[string]any) error {
+	{
+		appsKustPath := path.Join(root, "kustomization.yaml")
+		appsKust, err := r.ReadKustomization(appsKustPath)
+		if err != nil {
+			return err
+		}
+		appsKust.AddResources(app.Name)
+		if err := r.WriteKustomization(appsKustPath, *appsKust); err != nil {
+			return err
+		}
+	}
+	appRootDir := path.Join(root, app.Name)
+	{
+		if err := r.RemoveDir(appRootDir); err != nil {
+			return err
+		}
+		if err := r.CreateDir(appRootDir); err != nil {
+			return err
+		}
+		if err := r.WriteYaml(path.Join(appRootDir, configFileName), values); err != nil {
+			return err
+		}
+	}
+	{
+		appKust := NewKustomization()
+		for _, t := range app.Templates {
+			appKust.AddResources(t.Name())
+			out, err := r.Writer(path.Join(appRootDir, t.Name()))
+			if err != nil {
+				return err
+			}
+			defer out.Close()
+			if err := t.Execute(out, values); err != nil {
+				return err
+			}
+		}
+		if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
+			return err
+		}
+	}
+	return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
+}
+
+func auth(signer ssh.Signer) *gitssh.PublicKeys {
+	return &gitssh.PublicKeys{
+		Signer: signer,
+		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
+			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+				// TODO(giolekva): verify server public key
+				// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
+				return nil
+			},
+		},
+	}
+}
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index e07532e..59e94a8 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -1,28 +1,26 @@
 package soft
 
 import (
+	"encoding/base64"
 	"fmt"
-	"io/ioutil"
+	"golang.org/x/crypto/ssh"
 	"log"
 	"net"
 	"os"
-	"time"
+	"strings"
 
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
-	// "github.com/go-git/go-git/v5/config"
-	"github.com/go-git/go-git/v5/plumbing/object"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/go-git/go-git/v5/storage/memory"
-	"golang.org/x/crypto/ssh"
-	"sigs.k8s.io/yaml"
 )
 
 type Client struct {
-	ip     string
-	port   int
-	Signer ssh.Signer
-	log    *log.Logger
+	IP       string
+	port     int
+	Signer   ssh.Signer
+	log      *log.Logger
+	pemBytes []byte
 }
 
 func NewClient(ip string, port int, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
@@ -31,28 +29,45 @@
 		return nil, err
 	}
 	log.SetPrefix("SOFT-SERVE: ")
+	log.Printf("Created signer")
+	pub := signer.PublicKey().Marshal()
+	b := make([]byte, 100)
+	base64.StdEncoding.Encode(b, pub)
+	log.Printf("%s\n", string(b))
 	return &Client{
 		ip,
 		port,
 		signer,
 		log,
+		clientPrivateKey,
 	}, nil
 }
 
 func (ss *Client) AddUser(name, pubKey string) error {
 	log.Printf("Adding user %s", name)
-	if err := ss.RunCommand(fmt.Sprintf("user create %s", name)); err != nil {
+	if err := ss.RunCommand("user", "create", name); err != nil {
 		return err
 	}
-	return ss.RunCommand(fmt.Sprintf("user add-pubkey %s %s", name, pubKey))
+	return ss.AddPublicKey(name, pubKey)
 }
 
 func (ss *Client) MakeUserAdmin(name string) error {
 	log.Printf("Making user %s admin", name)
-	return ss.RunCommand(fmt.Sprintf("user set-admin %s true", name))
+	return ss.RunCommand("user", "set-admin", name, "true")
 }
 
-func (ss *Client) RunCommand(cmd string) error {
+func (ss *Client) AddPublicKey(user string, pubKey string) error {
+	log.Printf("Adding public key: %s %s\n", user, pubKey)
+	return ss.RunCommand("user", "add-pubkey", user, pubKey)
+}
+
+func (ss *Client) RemovePublicKey(user string, pubKey string) error {
+	log.Printf("Adding public key: %s %s\n", user, pubKey)
+	return ss.RunCommand("user", "remove-pubkey", user, pubKey)
+}
+
+func (ss *Client) RunCommand(args ...string) error {
+	cmd := strings.Join(args, " ")
 	log.Printf("Running command %s", cmd)
 	client, err := ssh.Dial("tcp", ss.addressSSH(), ss.sshClientConfig())
 	if err != nil {
@@ -63,84 +78,25 @@
 		return err
 	}
 	defer session.Close()
+	session.Stdout = os.Stdout
+	session.Stderr = os.Stderr
 	return session.Run(cmd)
 }
 
 func (ss *Client) AddRepository(name, readme string) error {
 	log.Printf("Adding repository %s", name)
-	return ss.RunCommand(fmt.Sprintf("repo create %s -d \"%s\"", name, readme))
+	return ss.RunCommand("repo", "create", name, "-d", fmt.Sprintf("\"%s\"", readme))
 }
 
 func (ss *Client) AddCollaborator(repo, user string) error {
 	log.Printf("Adding collaborator %s %s", repo, user)
-	return ss.RunCommand(fmt.Sprintf("repo collab add %s %s", repo, user))
-}
-
-func (ss *Client) CreateRepository(name string) error {
-	log.Printf("Creating repository %s", name)
-	configRepo, err := ss.getConfigRepo()
-	if err != nil {
-		return err
-	}
-	wt, err := configRepo.Worktree()
-	if err != nil {
-		return err
-	}
-	if err = wt.Checkout(&git.CheckoutOptions{
-		Branch: "refs/heads/master",
-	}); err != nil {
-		return err
-	}
-	f, err := wt.Filesystem.Open("config.yaml")
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-	configBytes, err := ioutil.ReadAll(f)
-	if err != nil {
-		return err
-	}
-	config := make(map[string]interface{})
-	if err := yaml.Unmarshal(configBytes, &config); err != nil {
-		return err
-	}
-	repos := config["repos"].([]interface{})
-	repos = append(repos, map[string]interface{}{
-		"name":    name,
-		"repo":    name,
-		"private": true,
-		"note":    fmt.Sprintf("PCloud env for %s", name),
-	})
-	config["repos"] = repos
-	configBytes, err = yaml.Marshal(config)
-	if err != nil {
-		return err
-	}
-	if err := ss.writeFile(wt, "config.yaml", string(configBytes)); err != nil {
-		return err
-	}
-	if err := ss.Commit(wt, fmt.Sprintf("add-repo: %s", name)); err != nil {
-		return err
-	}
-	return ss.Push(configRepo)
-}
-
-func (ss *Client) getConfigRepo() (*git.Repository, error) {
-	return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
-		URL:             ss.addressGit(),
-		Auth:            ss.authGit(),
-		RemoteName:      "origin",
-		ReferenceName:   "refs/heads/master",
-		Depth:           1,
-		InsecureSkipTLS: true,
-		Progress:        os.Stdout,
-	})
+	return ss.RunCommand("repo", "collab", "add", repo, user)
 }
 
 func (ss *Client) GetRepo(name string) (*git.Repository, error) {
 	return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
-		URL:             fmt.Sprintf("%s/%s", ss.addressGit(), name),
-		Auth:            ss.authGit(),
+		URL:             ss.GetRepoAddress(name),
+		Auth:            ss.authSSH(),
 		RemoteName:      "origin",
 		ReferenceName:   "refs/heads/master",
 		Depth:           1,
@@ -149,57 +105,38 @@
 	})
 }
 
-func (ss *Client) repoPathByName(name string) string {
-	return fmt.Sprintf("%s/%s", ss.addressGit(), name)
-}
-
-func (ss *Client) Commit(wt *git.Worktree, message string) error {
-	_, err := wt.Commit(message, &git.CommitOptions{
-		Author: &object.Signature{
-			Name:  "pcloud",
-			Email: "pcloud@installer",
-			When:  time.Now(),
-		},
-	})
-	return err
-}
-
-func (ss *Client) Push(repo *git.Repository) error {
-	return repo.Push(&git.PushOptions{
-		RemoteName: "origin",
-		Auth:       ss.authGit(),
-	})
-}
-
-func (ss *Client) writeFile(wt *git.Worktree, path, contents string) error {
-	f, err := wt.Filesystem.Create(path)
+func (ss *Client) authSSH() gitssh.AuthMethod {
+	a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
 	if err != nil {
-		return err
+		panic(err)
 	}
-	defer f.Close()
-	if _, err = f.Write([]byte(contents)); err != nil {
-		return err
+	a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+		// TODO(giolekva): verify server public key
+		ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
+		return nil
 	}
-	_, err = wt.Add(path)
-	return err
-}
-
-func (ss *Client) CloneRepository(name string) (*git.Repository, error) {
-	return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
-		URL:             ss.repoPathByName(name),
-		Auth:            ss.authGit(),
-		RemoteName:      "origin",
-		InsecureSkipTLS: true,
-	})
+	return a
+	// return &gitssh.PublicKeys{
+	// 	User:   "git",
+	// 	Signer: ss.Signer,
+	// 	HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
+	// 		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+	// 			// TODO(giolekva): verify server public key
+	// 			ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
+	// 			return nil
+	// 		},
+	// 	},
+	// }
 }
 
 func (ss *Client) authGit() *gitssh.PublicKeys {
 	return &gitssh.PublicKeys{
+		User:   "git",
 		Signer: ss.Signer,
 		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
 			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 				// TODO(giolekva): verify server public key
-				// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
+				ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
 				return nil
 			},
 		},
@@ -238,10 +175,14 @@
 	}
 }
 
+func (ss *Client) GetRepoAddress(name string) string {
+	return fmt.Sprintf("%s/%s", ss.addressGit(), name)
+}
+
 func (ss *Client) addressGit() string {
-	return fmt.Sprintf("ssh://%s:%d", ss.ip, ss.port)
+	return fmt.Sprintf("ssh://%s", ss.addressSSH())
 }
 
 func (ss *Client) addressSSH() string {
-	return fmt.Sprintf("%s:%d", ss.ip, ss.port)
+	return fmt.Sprintf("%s:%d", ss.IP, ss.port)
 }
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md
new file mode 100644
index 0000000..e6f01a3
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.md
@@ -0,0 +1 @@
+Installs rbacs to let cert-manager create gandi resource
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml
new file mode 100644
index 0000000..f0f9b93
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi-role.yaml
@@ -0,0 +1,23 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: cert-manager-webhook-gandi-role
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-cert-manager
+  dependsOn:
+    - name: cert-manager
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/cert-manager-webhook-gandi-role
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  timeout: 20m0s
+  values:
+    certManager:
+      namespace: {{ .Global.PCloudEnvName }}-cert-manager
+      name: {{ .Global.PCloudEnvName }}-cert-manager
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi.jsonschema b/core/installer/values-tmpl/cert-manager-webhook-gandi.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi.md b/core/installer/values-tmpl/cert-manager-webhook-gandi.md
new file mode 100644
index 0000000..6590062
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi.md
@@ -0,0 +1 @@
+Installs cert-manager DNS01 resolver for Gandi.net domain registrar
diff --git a/core/installer/values-tmpl/cert-manager-webhook-gandi.yaml b/core/installer/values-tmpl/cert-manager-webhook-gandi.yaml
new file mode 100644
index 0000000..fdcc030
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager-webhook-gandi.yaml
@@ -0,0 +1,29 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: cert-manager-webhook-gandi
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-cert-manager
+  dependsOn:
+    - name: cert-manager
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/cert-manager-webhook-gandi
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  timeout: 20m0s
+  values:
+    fullnameOverride: {{ .Global.PCloudEnvName }}-cert-manager-webhook-gandi
+    certManager:
+      namespace: {{ .Global.PCloudEnvName }}-cert-manager
+      name: {{ .Global.PCloudEnvName }}-cert-manager
+    image:
+      repository: giolekva/cert-manager-webhook-gandi
+      tag: v0.2.0
+      pullPolicy: IfNotPresent
+    logLevel: 2
diff --git a/core/installer/values-tmpl/cert-manager.jsonschema b/core/installer/values-tmpl/cert-manager.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/cert-manager.md b/core/installer/values-tmpl/cert-manager.md
new file mode 100644
index 0000000..aba785a
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager.md
@@ -0,0 +1 @@
+Installs cert-manager
diff --git a/core/installer/values-tmpl/cert-manager.yaml b/core/installer/values-tmpl/cert-manager.yaml
new file mode 100644
index 0000000..db00d34
--- /dev/null
+++ b/core/installer/values-tmpl/cert-manager.yaml
@@ -0,0 +1,47 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: namespaces-cert-manager
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  chart:
+    spec:
+      chart: charts/namespaces
+      sourceRef:
+        kind: GitRepository
+        name: {{ .Global.PCloudEnvName }}
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
+    pcloudInstanceId: ""
+    namespacePrefix: {{ .Global.PCloudEnvName }}-
+    namespaces:
+    - cert-manager
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: cert-manager
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-cert-manager
+  dependsOn:
+    - name: namespaces-cert-manager
+      namespace: {{ .Global.PCloudEnvName }}
+    - name: ingress-public
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/cert-manager
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  timeout: 20m0s
+  values:
+    fullnameOverride: {{ .Global.PCloudEnvName }}-cert-manager
+    installCRDs: true
+    image:
+      tag: v1.11.1
+      pullPolicy: IfNotPresent
diff --git a/core/installer/values-tmpl/certificate-issuer-private.jsonschema b/core/installer/values-tmpl/certificate-issuer-private.jsonschema
new file mode 100644
index 0000000..46ae9c3
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-private.jsonschema
@@ -0,0 +1,7 @@
+{
+  "type": "object",
+  "properties": {
+    "GandiAPIToken": { "type": "string" },
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/certificate-issuer-private.md b/core/installer/values-tmpl/certificate-issuer-private.md
new file mode 100644
index 0000000..9ee84cc
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-private.md
@@ -0,0 +1 @@
+Installs certificate issuer for private domain
diff --git a/core/installer/values-tmpl/certificate-issuer-private.yaml b/core/installer/values-tmpl/certificate-issuer-private.yaml
new file mode 100644
index 0000000..23004a0
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-private.yaml
@@ -0,0 +1,30 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: certificate-issuer
+  namespace: {{ .Global.Id }}
+spec:
+  targetNamespace: {{ .Global.NamespacePrefix }}ingress-private
+  dependsOn:
+  - name: ingress-private
+    namespace: {{ .Global.Id }}
+  chart:
+    spec:
+      chart: charts/certificate-issuer-private
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.Id }}
+  interval: 1m0s
+  values:
+    certManager:
+      namespace: {{ .Global.PCloudEnvName }}-cert-manager
+      gandiWebhookSecretReader: {{ .Global.PCloudEnvName }}-cert-manager-webhook-gandi
+    issuer:
+      name: {{ .Global.Id }}-private
+      # server: https://acme-v02.api.letsencrypt.org/directory
+      server: https://acme-staging-v02.api.letsencrypt.org/directory
+      domain: {{ .Global.PrivateDomain }}
+      contactEmail: {{ .Global.ContactEmail }}
+      ingressClassName: {{ .Global.Id }}-ingress-private
+      gandiAPIToken: {{ .Values.GandiAPIToken }}
diff --git a/core/installer/values-tmpl/core-auth.yaml b/core/installer/values-tmpl/core-auth.yaml
index 13e9c9c..856fb4c 100644
--- a/core/installer/values-tmpl/core-auth.yaml
+++ b/core/installer/values-tmpl/core-auth.yaml
@@ -8,6 +8,8 @@
   dependsOn:
   - name: core-auth-storage
     namespace: {{ .Global.Id }}
+  - name: ingress-private
+    namespace: {{ .Global.Id }}
   chart:
     spec:
       chart: charts/auth
diff --git a/core/installer/values-tmpl/csi-driver-smb.jsonschema b/core/installer/values-tmpl/csi-driver-smb.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/csi-driver-smb.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/csi-driver-smb.md b/core/installer/values-tmpl/csi-driver-smb.md
new file mode 100644
index 0000000..171a000
--- /dev/null
+++ b/core/installer/values-tmpl/csi-driver-smb.md
@@ -0,0 +1 @@
+Installs iCSI SMB driver
diff --git a/core/installer/values-tmpl/csi-driver-smb.yaml b/core/installer/values-tmpl/csi-driver-smb.yaml
new file mode 100644
index 0000000..9a2cf36
--- /dev/null
+++ b/core/installer/values-tmpl/csi-driver-smb.yaml
@@ -0,0 +1,40 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: namespaces-csi-driver-smb
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  chart:
+    spec:
+      chart: charts/namespaces
+      sourceRef:
+        kind: GitRepository
+        name: {{ .Global.PCloudEnvName }}
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
+    pcloudInstanceId: ""
+    namespacePrefix: {{ .Global.PCloudEnvName }}-
+    namespaces:
+    - csi-driver-smb
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: csi-driver-smb
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-csi-driver-smb
+  dependsOn:
+    - name: namespaces-csi-driver-smb
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/csi-driver-smb
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  timeout: 20m0s
+  values:
diff --git a/core/installer/values-tmpl/env-manager.jsonschema b/core/installer/values-tmpl/env-manager.jsonschema
new file mode 100644
index 0000000..aa2f01d
--- /dev/null
+++ b/core/installer/values-tmpl/env-manager.jsonschema
@@ -0,0 +1,8 @@
+{
+  "type": "object",
+  "properties": {
+    "RepoIP": { "type": "string", "default": "192.168.0.11" },
+	"SSHPrivateKey": { "type": "string", "default": "foo bar" }
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/env-manager.md b/core/installer/values-tmpl/env-manager.md
new file mode 100644
index 0000000..ec69eba
--- /dev/null
+++ b/core/installer/values-tmpl/env-manager.md
@@ -0,0 +1 @@
+PCloud environment manager
diff --git a/core/installer/values-tmpl/env-manager.yaml b/core/installer/values-tmpl/env-manager.yaml
new file mode 100644
index 0000000..691693b
--- /dev/null
+++ b/core/installer/values-tmpl/env-manager.yaml
@@ -0,0 +1,17 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: env-manager
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  chart:
+    spec:
+      chart: charts/env-manager
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
+    repoIP: {{ .Values.RepoIP }}
+    sshPrivateKey: {{ .Values.SSHPrivateKey | b64enc }}
diff --git a/core/installer/values-tmpl/headscale-controller.jsonschema b/core/installer/values-tmpl/headscale-controller.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-controller.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/headscale-controller.md b/core/installer/values-tmpl/headscale-controller.md
new file mode 100644
index 0000000..99d2190
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-controller.md
@@ -0,0 +1 @@
+Installs headscale controller
diff --git a/core/installer/values-tmpl/headscale-controller.yaml b/core/installer/values-tmpl/headscale-controller.yaml
new file mode 100644
index 0000000..3285f58
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-controller.yaml
@@ -0,0 +1,38 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: namespaces-headscale-controller
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  chart:
+    spec:
+      chart: charts/namespaces
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
+    namespacePrefix: {{ .Global.PCloudEnvName }}-
+    namespaces:
+    - headscale-controller
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: headscale-controller
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-headscale-controller
+  dependsOn:
+    - name: namespaces-headscale-controller
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/headscale-controller
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
diff --git a/core/installer/values-tmpl/headscale.yaml b/core/installer/values-tmpl/headscale.yaml
index 79e457e..8c16b86 100644
--- a/core/installer/values-tmpl/headscale.yaml
+++ b/core/installer/values-tmpl/headscale.yaml
@@ -28,6 +28,8 @@
   dependsOn:
     - name: namespaces-headscale
       namespace: {{ .Global.Id }}
+    - name: core-auth
+      namespace: {{ .Global.Id }}
   chart:
     spec:
       chart: charts/headscale
@@ -46,7 +48,7 @@
     ingressClassName: pcloud-ingress-public
     certificateIssuer: {{ .Global.Id }}-public
     domain: {{ .Values.Subdomain }}.{{ .Global.Domain }}
-    internalBaseDomain: {{ .Global.PrivateDomain }}
+    publicBaseDomain: {{ .Global.Domain }}
     oauth2:
       hydraAdmin: http://hydra-admin.{{ .Global.NamespacePrefix }}core-auth.svc.cluster.local
       hydraPublic: https://hydra.{{ .Global.Domain }}
diff --git a/core/installer/values-tmpl/ingress-private.yaml b/core/installer/values-tmpl/ingress-private.yaml
index f33e179..0874b72 100644
--- a/core/installer/values-tmpl/ingress-private.yaml
+++ b/core/installer/values-tmpl/ingress-private.yaml
@@ -21,34 +21,12 @@
 apiVersion: helm.toolkit.fluxcd.io/v2beta1
 kind: HelmRelease
 metadata:
-  name: volumes-ingress-private
-  namespace: {{ .Global.Id }}
-spec:
-  targetNamespace: {{ .Global.NamespacePrefix }}ingress-private
-  dependsOn:
-    - name: namespaces-ingress-private
-      namespace: {{ .Global.Id }}
-  chart:
-    spec:
-      chart: charts/volumes
-      sourceRef:
-        kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
-  interval: 1m0s
-  values:
-    name: tailscale
-    size: 1Gi
----
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
   name: ingress-private
   namespace: {{ .Global.Id }}
 spec:
   targetNamespace: {{ .Global.NamespacePrefix }}ingress-private
   dependsOn:
-    - name: volumes-ingress-private
+    - name: namespaces-ingress-private
       namespace: {{ .Global.Id }}
   chart:
     spec:
diff --git a/core/installer/values-tmpl/ingress-public.jsonschema b/core/installer/values-tmpl/ingress-public.jsonschema
index ec6a2c5..f42d895 100644
--- a/core/installer/values-tmpl/ingress-public.jsonschema
+++ b/core/installer/values-tmpl/ingress-public.jsonschema
@@ -1,15 +1,6 @@
 {
   "type": "object",
   "properties": {
-    "Values": {
-      "type": "object",
-      "properties": {
-        "NamespacePrefix": { "type": "string" },
-        "Id": { "type": "string" },
-        "Domain": { "type": "string" }
-      },
-      "additionalProperties": false
-    }
   },
   "additionalProperties": false
 }
diff --git a/core/installer/values-tmpl/ingress-public.md b/core/installer/values-tmpl/ingress-public.md
new file mode 100644
index 0000000..227c2d4
--- /dev/null
+++ b/core/installer/values-tmpl/ingress-public.md
@@ -0,0 +1 @@
+Sets up ingress for publicly accessible services
diff --git a/core/installer/values-tmpl/ingress-public.yaml b/core/installer/values-tmpl/ingress-public.yaml
index 43ae8bc..80a5efd 100644
--- a/core/installer/values-tmpl/ingress-public.yaml
+++ b/core/installer/values-tmpl/ingress-public.yaml
@@ -2,51 +2,52 @@
 kind: HelmRelease
 metadata:
   name: namespaces-ingress-public
-  namespace: {{ .Global.Id }}
+  namespace: {{ .Global.PCloudEnvName }}
 spec:
   chart:
     spec:
       chart: charts/namespaces
       sourceRef:
         kind: GitRepository
-        name: pcloud
-        namespace: {{ .Global.Id }}
+        name: {{ .Global.PCloudEnvName }}
+        namespace: {{ .Global.PCloudEnvName }}
   interval: 1m0s
   values:
-    pcloudInstanceId: {{ .Global.Id }}
-    namespacePrefix: {{ .Global.NamespacePrefix }}
+    pcloudInstanceId: ""
+    namespacePrefix: {{ .Global.PCloudEnvName }}-
     namespaces:
-    - app-ingress-public
+    - ingress-public
 ---
 apiVersion: helm.toolkit.fluxcd.io/v2beta1
 kind: HelmRelease
 metadata:
   name: ingress-public
-  namespace: {{ .Global.Id }}
+  namespace: {{ .Global.PCloudEnvName }}
 spec:
-  targetNamespace: {{ .Values.NamespacePrefix }}ingress-public
+  targetNamespace: {{ .Global.PCloudEnvName }}-ingress-public
   dependsOn:
     - name: namespaces-ingress-public
-      namespace: {{ .Global.Id }}
+      namespace: {{ .Global.PCloudEnvName }}
   chart:
     spec:
-      chart: ingress-nginx
-      version: 4.0.3
+      chart: charts/ingress-nginx
       sourceRef:
-        kind: HelmRepository
-        name: ingress-nginx
-        namespace: {{ .Values.Id }}
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
   interval: 1m0s
   values:
-    fullnameOverride: {{ .Values.Id }}-ingress-public
+    fullnameOverride: {{ .Global.PCloudEnvName }}-ingress-public
     controller:
       service:
         type: LoadBalancer
+        annotations:
+          metallb.universe.tf/loadBalancerIPs: 192.168.0.213 # TODO(giolekva): configurable
       ingressClassByName: true
       ingressClassResource:
-        name: {{ .Values.Id }}-ingress-public
+        name: {{ .Global.PCloudEnvName }}-ingress-public
         enabled: true
         default: false
-        controllerValue: k8s.io/{{ .Values.Id }}-ingress-public
+        controllerValue: k8s.io/{{ .Global.PCloudEnvName }}-ingress-public
       config:
-        proxy-body-size: 100M
+        proxy-body-size: 100M # TODO(giolekva): configurable
diff --git a/core/installer/values-tmpl/metallb-config-env.yaml b/core/installer/values-tmpl/metallb-config-env.yaml
index 6ea2ac9..39907e4 100644
--- a/core/installer/values-tmpl/metallb-config-env.yaml
+++ b/core/installer/values-tmpl/metallb-config-env.yaml
@@ -1,7 +1,7 @@
 apiVersion: helm.toolkit.fluxcd.io/v2beta1
 kind: HelmRelease
 metadata:
-  name: meteallb-config-env
+  name: meteallb-config-env # TODO(giolekva): typo metea
   namespace: {{ .Global.Id }}
 spec:
   chart:
diff --git a/core/installer/values-tmpl/resource-renderer-controller.jsonschema b/core/installer/values-tmpl/resource-renderer-controller.jsonschema
new file mode 100644
index 0000000..f42d895
--- /dev/null
+++ b/core/installer/values-tmpl/resource-renderer-controller.jsonschema
@@ -0,0 +1,6 @@
+{
+  "type": "object",
+  "properties": {
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/resource-renderer-controller.md b/core/installer/values-tmpl/resource-renderer-controller.md
new file mode 100644
index 0000000..81231e0
--- /dev/null
+++ b/core/installer/values-tmpl/resource-renderer-controller.md
@@ -0,0 +1 @@
+Installs resource-renderer controller
diff --git a/core/installer/values-tmpl/resource-renderer-controller.yaml b/core/installer/values-tmpl/resource-renderer-controller.yaml
new file mode 100644
index 0000000..72cf68b
--- /dev/null
+++ b/core/installer/values-tmpl/resource-renderer-controller.yaml
@@ -0,0 +1,38 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: namespaces-rr-controller
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  chart:
+    spec:
+      chart: charts/namespaces
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
+    namespacePrefix: {{ .Global.PCloudEnvName }}-
+    namespaces:
+    - rr-controller
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: rr-controller
+  namespace: {{ .Global.PCloudEnvName }}
+spec:
+  targetNamespace: {{ .Global.PCloudEnvName }}-rr-controller
+  dependsOn:
+    - name: namespaces-rr-controller
+      namespace: {{ .Global.PCloudEnvName }}
+  chart:
+    spec:
+      chart: charts/resource-renderer-controller
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.PCloudEnvName }}
+  interval: 1m0s
+  values:
diff --git a/core/installer/values-tmpl/tailscale-proxy.yaml b/core/installer/values-tmpl/tailscale-proxy.yaml
index 56dec5e..9664e51 100644
--- a/core/installer/values-tmpl/tailscale-proxy.yaml
+++ b/core/installer/values-tmpl/tailscale-proxy.yaml
@@ -28,6 +28,8 @@
   dependsOn:
     - name: namespaces-tailscale-proxy
       namespace: {{ .Global.Id }}
+    - name: headscale
+      namespace: {{ .Global.Id }}
   chart:
     spec:
       chart: charts/tailscale
@@ -38,6 +40,7 @@
   interval: 1m0s
   values:
     hostname: {{ .Global.PCloudEnvName }}-{{ .Global.Id }}-internal-proxy
+    apiServer: http://headscale-api.{{ .Global.Id }}-app-headscale.svc.cluster.local
     loginServer: https://headscale.{{ .Global.Domain }} # TODO(gio): take headscale subdomain from configuration
     ipSubnet: {{ .Values.IPSubnet }}
     username: {{ .Values.Username }}
diff --git a/core/resource-renderer/Dockerfile b/core/resource-renderer/Dockerfile
index 5a355c2..c4ac419 100644
--- a/core/resource-renderer/Dockerfile
+++ b/core/resource-renderer/Dockerfile
@@ -1,6 +1,8 @@
 # Build the manager binary
 FROM golang:1.18 as builder
 
+ARG TARGETARCH
+
 WORKDIR /workspace
 # Copy the Go Modules manifests
 COPY go.mod go.mod
@@ -15,7 +17,7 @@
 COPY controllers/ controllers/
 
 # Build
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -o manager main.go
 
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details