installer: configure pcloud repo during bootstrap
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 53f1248..6bf0161 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -122,7 +122,7 @@
 		if err := t.Execute(out, all); err != nil {
 			return err
 		}
-		appKust.Resources = append(appKust.Resources, t.Name())
+		appKust.AddResources(t.Name())
 	}
 	{
 		out, err := appRoot.Create(configFileName)
@@ -147,7 +147,7 @@
 		return err
 	}
 	if !slices.Contains(rootKust.Resources, app.Name) {
-		rootKust.Resources = append(rootKust.Resources, app.Name)
+		rootKust.AddResources(app.Name)
 		rootKustFW, err := appsRoot.Create(kustomizationFileName)
 		if err != nil {
 			return err
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index 9d99e17..e5f3f2b 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -12,12 +12,13 @@
 	"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"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/kube"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/soft"
 )
 
 var bootstrapFlags struct {
@@ -78,10 +79,6 @@
 	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
@@ -92,7 +89,7 @@
 	if err := installMetallb(); err != nil {
 		return err
 	}
-	time.Sleep(3 * time.Minute)
+	time.Sleep(1 * time.Minute)
 	if err := installMetallbConfig(); err != nil {
 		return err
 	}
@@ -104,7 +101,11 @@
 		return err
 	}
 	time.Sleep(2 * time.Minute)
-	ss, err := soft.NewClient(bootstrapFlags.softServeIP, 2222, adminPrivKey, log.Default())
+	ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, adminPrivKey, log.Default())
+	if err != nil {
+		return err
+	}
+	fluxPub, fluxPriv, err := installer.GenerateSSHKeys()
 	if err != nil {
 		return err
 	}
@@ -122,6 +123,13 @@
 	if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
 		return err
 	}
+	pcloudRepo, err := ss.GetRepo("pcloud") // TODO(giolekva): configurable
+	if err != nil {
+		return err
+	}
+	if err := configurePCloudRepo(installer.NewRepoIO(pcloudRepo, ss.Signer)); err != nil {
+		return err
+	}
 	// TODO(giolekva): everything below must be installed using Flux
 	if err := installIngressPublic(); err != nil {
 		return err
@@ -132,6 +140,7 @@
 	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 {
 		return err
 	}
@@ -162,6 +171,7 @@
 	installer.Namespace = "pcloud"
 	installer.ReleaseName = "metallb-ns"
 	installer.Wait = true
+	installer.WaitForJobs = true
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
 	}
@@ -202,7 +212,8 @@
 	installer.CreateNamespace = true
 	installer.ReleaseName = "metallb"
 	installer.IncludeCRDs = true
-	// installer.Wait = true
+	installer.Wait = true
+	installer.WaitForJobs = true
 	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
@@ -230,6 +241,7 @@
 	installer.CreateNamespace = true
 	installer.ReleaseName = "metallb-cfg"
 	installer.Wait = true
+	installer.WaitForJobs = true
 	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
@@ -268,6 +280,7 @@
 	installer.CreateNamespace = true
 	installer.ReleaseName = "longhorn"
 	installer.Wait = true
+	installer.WaitForJobs = true
 	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
@@ -296,6 +309,7 @@
 	installer.CreateNamespace = true
 	installer.ReleaseName = "soft-serve"
 	installer.Wait = true
+	installer.WaitForJobs = true
 	installer.Timeout = 20 * time.Minute
 	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
 		return err
@@ -465,6 +479,18 @@
 	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 {
+		return err
+	}
+	if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
+		return err
+	}
+	return repo.CommitAndPush("initialize pcloud directory structure, environments with kustomization.yaml-s")
+}
+
 func createActionConfig(namespace string) (*action.Configuration, error) {
 	config := new(action.Configuration)
 	if err := config.Init(
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
index 1ae0888..4b258b1 100644
--- a/core/installer/cmd/env.go
+++ b/core/installer/cmd/env.go
@@ -126,7 +126,7 @@
 			return err
 		}
 	}
-	kust.Resources = append(kust.Resources, createEnvFlags.name)
+	kust.AddResources(createEnvFlags.name)
 	ff, err := wt.Filesystem.Create(envKust)
 	if err != nil {
 		return err
diff --git a/core/installer/kustomization.go b/core/installer/kustomization.go
index 8b96668..f5f42e5 100644
--- a/core/installer/kustomization.go
+++ b/core/installer/kustomization.go
@@ -44,3 +44,7 @@
 	}
 	return nil
 }
+
+func (k *Kustomization) AddResources(names ...string) {
+	k.Resources = append(k.Resources, names...)
+}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
new file mode 100644
index 0000000..271a1c4
--- /dev/null
+++ b/core/installer/repoio.go
@@ -0,0 +1,80 @@
+package installer
+
+import (
+	"io/fs"
+	"path/filepath"
+	"time"
+
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"golang.org/x/crypto/ssh"
+)
+
+type RepoIO interface {
+	ReadKustomization(path string) (*Kustomization, error)
+	WriteKustomization(path string, kust Kustomization) error
+	CommitAndPush(message string) error
+}
+
+type repoIO struct {
+	repo   *git.Repository
+	signer ssh.Signer
+}
+
+func NewRepoIO(repo *git.Repository, signer ssh.Signer) RepoIO {
+	return &repoIO{
+		repo,
+		signer,
+	}
+}
+
+func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return nil, err
+	}
+	inp, err := wt.Filesystem.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer inp.Close()
+	return ReadKustomization(inp)
+}
+
+func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
+		return err
+	}
+	out, err := wt.Filesystem.Create(path)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+	return kust.Write(out)
+}
+
+func (r *repoIO) CommitAndPush(message string) error {
+	wt, err := r.repo.Worktree()
+	if err != nil {
+		return err
+	}
+	if err := wt.AddGlob("*"); err != nil {
+		return err
+	}
+	if _, err := wt.Commit(message, &git.CommitOptions{
+		Author: &object.Signature{
+			Name: "pcloud-installer",
+			When: time.Now(),
+		},
+	}); err != nil {
+		return err
+	}
+	return r.repo.Push(&git.PushOptions{
+		RemoteName: "soft", // TODO(giolekva): configurable
+		Auth:       auth(r.signer),
+	})
+}
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 01d79c7..68964d3 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -11,7 +11,6 @@
 	"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"
 	"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"
@@ -22,7 +21,7 @@
 type Client struct {
 	ip     string
 	port   int
-	signer ssh.Signer
+	Signer ssh.Signer
 	log    *log.Logger
 }
 
@@ -87,18 +86,11 @@
 	if err != nil {
 		return err
 	}
-	fmt.Println("aaaa")
-	b, _ := configRepo.Branches()
-	b.ForEach(func(r *plumbing.Reference) error {
-		fmt.Println(r.Name())
-		return nil
-	})
 	if err = wt.Checkout(&git.CheckoutOptions{
 		Branch: "refs/heads/master",
 	}); err != nil {
 		return err
 	}
-	fmt.Println("bbb")
 	f, err := wt.Filesystem.Open("config.yaml")
 	if err != nil {
 		return err
@@ -145,6 +137,18 @@
 	})
 }
 
+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(),
+		RemoteName:      "soft",
+		ReferenceName:   "refs/heads/master",
+		Depth:           1,
+		InsecureSkipTLS: true,
+		Progress:        os.Stdout,
+	})
+}
+
 func (ss *Client) repoPathByName(name string) string {
 	return fmt.Sprintf("%s/%s", ss.addressGit(), name)
 }
@@ -191,7 +195,7 @@
 
 func (ss *Client) authGit() *gitssh.PublicKeys {
 	return &gitssh.PublicKeys{
-		Signer: ss.signer,
+		Signer: ss.Signer,
 		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
 			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 				// TODO(giolekva): verify server public key
@@ -206,7 +210,7 @@
 	var ret []byte
 	config := &ssh.ClientConfig{
 		Auth: []ssh.AuthMethod{
-			ssh.PublicKeys(ss.signer),
+			ssh.PublicKeys(ss.Signer),
 		},
 		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 			ret = ssh.MarshalAuthorizedKey(key)
@@ -223,7 +227,7 @@
 func (ss *Client) sshClientConfig() *ssh.ClientConfig {
 	return &ssh.ClientConfig{
 		Auth: []ssh.AuthMethod{
-			ssh.PublicKeys(ss.signer),
+			ssh.PublicKeys(ss.Signer),
 		},
 		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 			// TODO(giolekva): verify server public key