installer: env and app manager
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
index eb49736..b96ac6f 100644
--- a/core/installer/cmd/apps.go
+++ b/core/installer/cmd/apps.go
@@ -1,19 +1,30 @@
 package main
 
 import (
+	"fmt"
 	"io/ioutil"
+	"net"
 	"os"
-	"path/filepath"
+	"time"
 
 	"github.com/giolekva/pcloud/core/installer"
+	"github.com/go-git/go-billy/v5/memfs"
+	"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"
+	"github.com/go-git/go-git/v5/storage/memory"
 	"github.com/spf13/cobra"
+	"golang.org/x/crypto/ssh"
 	"sigs.k8s.io/yaml"
 )
 
+const appDirName = "apps"
+
 var installFlags struct {
-	config    string
-	appName   string
-	outputDir string
+	sshKey   string
+	config   string
+	appName  string
+	repoAddr string
 }
 
 func installCmd() *cobra.Command {
@@ -22,6 +33,12 @@
 		RunE: installCmdRun,
 	}
 	cmd.Flags().StringVar(
+		&installFlags.sshKey,
+		"ssh-key",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
 		&installFlags.config,
 		"config",
 		"",
@@ -34,36 +51,92 @@
 		"",
 	)
 	cmd.Flags().StringVar(
-		&installFlags.outputDir,
-		"output-dir",
+		&installFlags.repoAddr,
+		"repo-addr",
 		"",
 		"",
 	)
 	return cmd
 }
 
+type inMemoryAppRepository struct {
+	apps []installer.App
+}
+
+func NewInMemoryAppRepository(apps []installer.App) installer.AppRepository {
+	return &inMemoryAppRepository{
+		apps,
+	}
+}
+
+func (r inMemoryAppRepository) Find(name string) (*installer.App, error) {
+	for _, a := range r.apps {
+		if a.Name == name {
+			return &a, nil
+		}
+	}
+	return nil, fmt.Errorf("Application not found: %s", name)
+}
+
 func installCmdRun(cmd *cobra.Command, args []string) error {
 	cfg, err := readConfig(installFlags.config)
 	if err != nil {
 		return err
 	}
-	apps := installer.CreateAllApps()
-	for _, a := range apps {
-		if a.Name == installFlags.appName {
-			for _, t := range a.Templates {
-				out, err := os.Create(filepath.Join(installFlags.outputDir, t.Name()))
-				if err != nil {
-					return err
-				}
-				defer out.Close()
-				if err := t.Execute(out, cfg); err != nil {
-					return err
-				}
-			}
-			break
-		}
+	sshKey, err := os.ReadFile(installFlags.sshKey)
+	if err != nil {
+		return err
 	}
-	return nil
+	signer, err := ssh.ParsePrivateKey(sshKey)
+	if err != nil {
+		return err
+	}
+	repo, err := cloneRepo(installFlags.repoAddr, signer)
+	if err != nil {
+		return err
+	}
+	wt, err := repo.Worktree()
+	if err != nil {
+		return err
+	}
+	appRoot, err := wt.Filesystem.Chroot(appDirName)
+	if err != nil {
+		return err
+	}
+	m, err := installer.NewAppManager(
+		appRoot,
+		cfg,
+		NewInMemoryAppRepository(installer.CreateAllApps()),
+	)
+	if err != nil {
+		return err
+	}
+	if err := m.Install(installFlags.appName); err != nil {
+		return err
+	}
+	if st, err := wt.Status(); err != nil {
+		return err
+	} else {
+		fmt.Printf("%+v\n", st)
+	}
+	wt.AddGlob("*")
+	if st, err := wt.Status(); err != nil {
+		return err
+	} else {
+		fmt.Printf("%+v\n", st)
+	}
+	if _, err := wt.Commit(fmt.Sprintf("install: %s", installFlags.appName), &git.CommitOptions{
+		Author: &object.Signature{
+			Name: "pcloud-appmanager",
+			When: time.Now(),
+		},
+	}); err != nil {
+		return err
+	}
+	return repo.Push(&git.PushOptions{
+		RemoteName: "origin",
+		Auth:       auth(signer),
+	})
 }
 
 func readConfig(config string) (installer.Config, error) {
@@ -75,3 +148,25 @@
 	err = yaml.UnmarshalStrict(inp, &cfg)
 	return cfg, err
 }
+
+func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
+	return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
+		URL:             address,
+		Auth:            auth(signer),
+		RemoteName:      "origin",
+		InsecureSkipTLS: true,
+	})
+}
+
+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/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index 151dd90..a11cfac 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -1,19 +1,18 @@
+// TODO
+// * ns pcloud not found
+
 package main
 
 import (
 	"context"
-	"crypto/ed25519"
-	"crypto/rand"
-	"crypto/x509"
 	_ "embed"
-	"encoding/pem"
 	"fmt"
-	"golang.org/x/crypto/ssh"
 	"log"
 	"os"
 	"path/filepath"
 	"time"
 
+	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
 	"github.com/spf13/cobra"
 	"helm.sh/helm/v3/pkg/action"
@@ -22,9 +21,12 @@
 )
 
 var bootstrapFlags struct {
-	chartsDir    string
-	adminPubKey  string
-	adminPrivKey string
+	chartsDir                 string
+	adminPubKey               string
+	adminPrivKey              string
+	storageDir                string
+	volumeDefaultReplicaCount int
+	softServeIP               string
 }
 
 func bootstrapCmd() *cobra.Command {
@@ -50,52 +52,229 @@
 		"",
 		"",
 	)
+	cmd.Flags().StringVar(
+		&bootstrapFlags.storageDir,
+		"storage-dir",
+		"",
+		"",
+	)
+	cmd.Flags().IntVar(
+		&bootstrapFlags.volumeDefaultReplicaCount,
+		"volume-default-replica-count",
+		3,
+		"",
+	)
+	cmd.Flags().StringVar(
+		&bootstrapFlags.softServeIP,
+		"soft-serve-ip",
+		"",
+		"",
+	)
 	return cmd
 }
 
 func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
-	adminPubKey, adminPrivKey, err := readAdminKeys()
+		adminPubKey, adminPrivKey, err := readAdminKeys()
+		if err != nil {
+			return err
+		}
+		fluxPub, fluxPriv, err := installer.GenerateSSHKeys()
+		if err != nil {
+			return err
+		}
+		softServePub, softServePriv, err := installer.GenerateSSHKeys()
+		if err != nil {
+			return err
+		}
+		if err := installMetallbNamespace(); err != nil {
+			return err
+		}
+		if err := installMetallb(); err != nil {
+			return err
+		}
+		time.Sleep(3 * time.Minute)
+		if err := installMetallbConfig(); err != nil {
+			return err
+		}
+		if err := installLonghorn(); err != nil {
+			return err
+		}
+		if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
+			return err
+		}
+		time.Sleep(30 * time.Second)
+		ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, adminPrivKey, log.Default())
+		if err != nil {
+			return err
+		}
+		if err := ss.AddUser("flux", fluxPub); err != nil {
+			return err
+		}
+		if err := ss.MakeUserAdmin("flux"); err != nil {
+			return err
+		}
+		fmt.Println("Creating /pcloud repo")
+		if err := ss.AddRepository("pcloud", "# PCloud Systems\n"); 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 {
+			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
+		}
+	}
+	return nil
+}
+
+func installMetallbNamespace() error {
+	fmt.Println("Installing metallb namespace")
+	// config, err := createActionConfig("default")
+	config, err := createActionConfig("pcloud")
 	if err != nil {
 		return err
 	}
-	fluxPub, fluxPriv, err := generateSSHKeys()
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "namespace"))
 	if err != nil {
 		return err
 	}
-	softServePub, softServePriv, err := generateSSHKeys()
+	values := map[string]interface{}{
+		// "namespace": "pcloud-metallb",
+		"namespace": "metallb-system",
+		"labels": []string{
+			"pod-security.kubernetes.io/audit: privileged",
+			"pod-security.kubernetes.io/enforce: privileged",
+			"pod-security.kubernetes.io/warn: privileged",
+		},
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "pcloud"
+	installer.ReleaseName = "metallb-ns"
+	installer.Wait = true
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
+		return err
+	}
+	return nil
+}
+
+func installMetallb() error {
+	fmt.Println("Installing metallb")
+	// config, err := createActionConfig("default")
+	config, err := createActionConfig("metallb-system")
 	if err != nil {
 		return err
 	}
-	fmt.Println("Installing SoftServe")
-	if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
-		return err
-	}
-	time.Sleep(30 * time.Second)
-	ss, err := soft.NewClient("192.168.0.208", 22, adminPrivKey, log.Default())
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "metallb"))
 	if err != nil {
 		return err
 	}
-	if err := ss.UpdateConfig(
-		soft.DefaultConfig([]string{string(adminPubKey), fluxPub}),
-		"set admin keys"); err != nil {
+	values := map[string]interface{}{ // TODO(giolekva): add loadBalancerClass?
+		"controller": map[string]interface{}{
+			"image": map[string]interface{}{
+				"repository": "quay.io/metallb/controller",
+				"tag":        "v0.13.9",
+				"pullPolicy": "IfNotPresent",
+			},
+			"logLevel": "info",
+		},
+		"speaker": map[string]interface{}{
+			"image": map[string]interface{}{
+				"repository": "quay.io/metallb/speaker",
+				"tag":        "v0.13.9",
+				"pullPolicy": "IfNotPresent",
+			},
+			"logLevel": "info",
+		},
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "metallb-system" // "pcloud-metallb"
+	installer.CreateNamespace = true
+	installer.ReleaseName = "metallb"
+	installer.IncludeCRDs = true
+	// installer.Wait = true
+	installer.Timeout = 20 * time.Minute
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
 	}
-	if err := ss.ReloadConfig(); err != nil {
+	return nil
+}
+
+func installMetallbConfig() error {
+	fmt.Println("Installing metallb-config")
+	// config, err := createActionConfig("default")
+	config, err := createActionConfig("metallb-system")
+	if err != nil {
 		return err
 	}
-	fmt.Println("Creating /pcloud repo")
-	if err := ss.AddRepository("pcloud", "# PCloud Systems\n"); err != nil {
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "metallb-config"))
+	if 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 {
+	values := map[string]interface{}{
+		"from": "192.168.0.210",
+		"to":   "192.168.0.240",
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "metallb-system" // "pcloud-metallb"
+	installer.CreateNamespace = true
+	installer.ReleaseName = "metallb-cfg"
+	installer.Wait = true
+	installer.Timeout = 20 * time.Minute
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
+		return err
+	}
+	return nil
+}
+
+func installLonghorn() error {
+	fmt.Println("Installing Longhorn")
+	config, err := createActionConfig("pcloud")
+	if err != nil {
+		return err
+	}
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "longhorn"))
+	if err != nil {
+		return err
+	}
+	values := map[string]interface{}{
+		"defaultSettings": map[string]interface{}{
+			"defaultDataPath": bootstrapFlags.storageDir,
+		},
+		"persistence": map[string]interface{}{
+			"defaultClassReplicaCount": bootstrapFlags.volumeDefaultReplicaCount,
+		},
+		"service": map[string]interface{}{
+			"ui": map[string]interface{}{
+				"type": "LoadBalancer",
+			},
+		},
+		"ingress": map[string]interface{}{
+			"enabled": false,
+		},
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "longhorn-system"
+	installer.CreateNamespace = true
+	installer.ReleaseName = "longhorn"
+	installer.Wait = true
+	installer.Timeout = 20 * time.Minute
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
 	}
 	return nil
 }
 
 func installSoftServe(pubKey, privKey, adminKey string) error {
-	config, err := createActionConfig()
+	fmt.Println("Installing SoftServe")
+	config, err := createActionConfig("pcloud")
 	if err != nil {
 		return err
 	}
@@ -107,13 +286,14 @@
 		"privateKey": privKey,
 		"publicKey":  pubKey,
 		"adminKey":   adminKey,
+		"reservedIP": bootstrapFlags.softServeIP,
 	}
 	installer := action.NewInstall(config)
 	installer.Namespace = "pcloud"
 	installer.CreateNamespace = true
 	installer.ReleaseName = "soft-serve"
 	installer.Wait = true
-	installer.Timeout = 5 * time.Minute
+	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
 	}
@@ -121,7 +301,7 @@
 }
 
 func installFlux(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
-	config, err := createActionConfig()
+	config, err := createActionConfig("pcloud")
 	if err != nil {
 		return err
 	}
@@ -141,18 +321,123 @@
 	installer.ReleaseName = "flux"
 	installer.Wait = true
 	installer.WaitForJobs = true
-	installer.Timeout = 5 * time.Minute
+	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
 	}
 	return nil
 }
 
-func createActionConfig() (*action.Configuration, error) {
+func installIngressPublic() error {
+	config, err := createActionConfig("pcloud")
+	if err != nil {
+		return err
+	}
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "ingress-nginx"))
+	if err != nil {
+		return err
+	}
+	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",
+			},
+		},
+	}
+	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 createActionConfig(namespace string) (*action.Configuration, error) {
 	config := new(action.Configuration)
 	if err := config.Init(
-		kube.GetConfig(rootFlags.kubeConfig, "", ""),
-		"pcloud",
+		kube.GetConfig(rootFlags.kubeConfig, "", namespace),
+		namespace,
 		"",
 		func(fmtString string, args ...interface{}) {
 			fmt.Printf(fmtString, args...)
@@ -164,28 +449,6 @@
 	return config, nil
 }
 
-func generateSSHKeys() (string, string, error) {
-	pub, priv, err := ed25519.GenerateKey(rand.Reader)
-	if err != nil {
-		return "", "", err
-	}
-	privEnc, err := x509.MarshalPKCS8PrivateKey(priv)
-	if err != nil {
-		return "", "", err
-	}
-	privPem := pem.EncodeToMemory(
-		&pem.Block{
-			Type:  "PRIVATE KEY",
-			Bytes: privEnc,
-		},
-	)
-	pubKey, err := ssh.NewPublicKey(pub)
-	if err != nil {
-		return "", "", err
-	}
-	return string(ssh.MarshalAuthorizedKey(pubKey)), string(privPem), nil
-}
-
 func readAdminKeys() ([]byte, []byte, error) {
 	pubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
 	if err != nil {
diff --git a/core/installer/cmd/env-tmpl/config-kustomization.yaml b/core/installer/cmd/env-tmpl/config-kustomization.yaml
new file mode 100644
index 0000000..d76bf0f
--- /dev/null
+++ b/core/installer/cmd/env-tmpl/config-kustomization.yaml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+spec:
+  interval: 1m
+  path: "./"
+  sourceRef:
+    kind: GitRepository
+    name: {{ .Name }}
+    namespace: {{ .Name }}
+  prune: true
diff --git a/core/installer/cmd/env-tmpl/config-secret.yaml b/core/installer/cmd/env-tmpl/config-secret.yaml
new file mode 100644
index 0000000..3ea515b
--- /dev/null
+++ b/core/installer/cmd/env-tmpl/config-secret.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+data:
+  identity: {{ .PrivateKey }}
+  identity.pub: {{ .PublicKey }}
+  known_hosts: {{ .KnownHosts }}
+kind: Secret
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+type: Opaque
diff --git a/core/installer/cmd/env-tmpl/config-source.yaml b/core/installer/cmd/env-tmpl/config-source.yaml
new file mode 100644
index 0000000..113a0b4
--- /dev/null
+++ b/core/installer/cmd/env-tmpl/config-source.yaml
@@ -0,0 +1,14 @@
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+spec:
+  gitImplementation: go-git
+  interval: 1m0s
+  ref:
+    branch: master
+  secretRef:
+    name: {{ .Name }}
+  timeout: 60s
+  url: ssh://{{ .GitHost }}/{{ .Name }}
diff --git a/core/installer/cmd/env-tmpl/kustomization.yaml b/core/installer/cmd/env-tmpl/kustomization.yaml
new file mode 100644
index 0000000..8ac663b
--- /dev/null
+++ b/core/installer/cmd/env-tmpl/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - config-secret.yaml
+  - config-source.yaml
+  - config-kustomization.yaml
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
index 6f24d19..1ae0888 100644
--- a/core/installer/cmd/env.go
+++ b/core/installer/cmd/env.go
@@ -1,14 +1,30 @@
+// TODO
+// * flux -n lekva create source git pcloud --url=ssh://192.168.0.211/pcloud-apps --branch=main --private-key-file=/Users/lekva/.ssh/id_rsa
+
 package main
 
 import (
+	"bytes"
+	"embed"
+	"encoding/base64"
 	"fmt"
+	"io"
 	"log"
 	"os"
+	"path"
+	"text/template"
 
+	"golang.org/x/exp/slices"
+
+	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
 	"github.com/spf13/cobra"
+	"sigs.k8s.io/yaml"
 )
 
+//go:embed env-tmpl
+var filesTmpls embed.FS
+
 var createEnvFlags struct {
 	name         string
 	adminPrivKey string
@@ -39,7 +55,19 @@
 	if err != nil {
 		return err
 	}
-	ss, err := soft.NewClient("192.168.0.208", 22, adminPrivKey, log.Default())
+	ss, err := soft.NewClient("192.168.0.211", 22, adminPrivKey, log.Default())
+	if err != nil {
+		return err
+	}
+	ssPubKey, err := ss.GetPublicKey()
+	if err != nil {
+		return err
+	}
+	fmt.Println(string(ssPubKey))
+	pub, priv, err := installer.GenerateSSHKeys()
+	{
+		_ = priv
+	}
 	if err != nil {
 		return err
 	}
@@ -47,5 +75,77 @@
 	if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
 		return err
 	}
+	fluxUserName := fmt.Sprintf("flux-%s", createEnvFlags.name)
+	if err := ss.AddUser(fluxUserName, pub); err != nil {
+		return err
+	}
+	if err := ss.AddCollaborator(createEnvFlags.name, fluxUserName); err != nil {
+		return err
+	}
+	repo, err := ss.CloneRepository("pcloud")
+	if err != nil {
+		return err
+	}
+	wt, err := repo.Worktree()
+	if err != nil {
+		return err
+	}
+	envKust := "environments/kustomization.yaml"
+	envKustFile, err := wt.Filesystem.Open(envKust)
+	if err != nil {
+		return err
+	}
+	kust, err := installer.ReadKustomization(envKustFile)
+	if err != nil {
+		return err
+	}
+	if slices.Contains(kust.Resources, createEnvFlags.name) {
+		return fmt.Errorf("Environment already exists: %s", 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())
+		fmt.Println(dstPath)
+		dst, err := wt.Filesystem.Create(dstPath)
+		if err != nil {
+			return err
+		}
+		if err := tmpl.Execute(dst, map[string]string{
+			"Name":       createEnvFlags.name,
+			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(priv)),
+			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(pub)),
+			"GitHost":    "192.168.0.211",
+			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("192.168.0.211 %s", ssPubKey))),
+		}); err != nil {
+			return err
+		}
+		if _, err := wt.Add(dstPath); err != nil {
+			return err
+		}
+	}
+	kust.Resources = append(kust.Resources, createEnvFlags.name)
+	ff, err := wt.Filesystem.Create(envKust)
+	if err != nil {
+		return err
+	}
+	contents, err := yaml.Marshal(kust)
+	if err != nil {
+		return err
+	}
+	if _, err := io.Copy(ff, bytes.NewReader(contents)); err != nil {
+		return err
+	}
+	if _, err := wt.Add(envKust); err != nil {
+		return err
+	}
+	if err := ss.Commit(wt, fmt.Sprintf("%s: new environment", createEnvFlags.name)); err != nil {
+		return err
+	}
+	if err := ss.Push(repo); err != nil {
+		return err
+	}
 	return nil
 }