installer: fully automate initial bootstrap and env creation
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
 }