installer: separate suffix and prefix generation.

Reuse suffix for app dir name.
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index b5815e4..fccf0b2 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -3,6 +3,7 @@
 import (
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 
 	"sigs.k8s.io/yaml"
 )
@@ -42,17 +43,21 @@
 	return cfg, err
 }
 
-func (m *AppManager) Install(app App, ns NamespaceGenerator, config map[string]any) error {
+func (m *AppManager) Install(app App, ns NamespaceGenerator, suffixGen SuffixGenerator, config map[string]any) error {
 	// if err := m.repoIO.Fetch(); err != nil {
 	// 	return err
 	// }
+	suffix, err := suffixGen.Generate()
+	if err != nil {
+		return err
+	}
 	namespaces := make([]string, len(app.Namespaces))
 	for i, n := range app.Namespaces {
-		var err error
-		namespaces[i], err = ns.Generate(n)
+		ns, err := ns.Generate(n)
 		if err != nil {
 			return err
 		}
+		namespaces[i] = ns + suffix
 	}
 	for _, n := range namespaces {
 		if err := m.nsCreator.Create(n); err != nil {
@@ -73,5 +78,8 @@
 		}
 	}
 	// TODO(giolekva): use ns suffix for app directory
-	return m.repoIO.InstallApp(app, "apps", all)
+	return m.repoIO.InstallApp(
+		app,
+		filepath.Join("/apps", app.Name+suffix),
+		all)
 }
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 9a15c6e..8cb6cad 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -257,11 +257,9 @@
 			}
 		}
 	}
-	nsGen := installer.NewCombine(
-		installer.NewPrefixGenerator(config.Values.Id+"-"),
-		installer.NewRandomSuffixGenerator(3),
-	)
-	if err := s.m.Install(a.App, nsGen, values); err != nil {
+	nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
+	suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
+	if err := s.m.Install(a.App, nsGen, suffixGen, values); err != nil {
 		return err
 	}
 	return c.String(http.StatusOK, "Installed")
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
index ffff0ba..d007ed2 100644
--- a/core/installer/cmd/bootstrap.go
+++ b/core/installer/cmd/bootstrap.go
@@ -403,7 +403,7 @@
 				"Namespace": namespaces[0],
 			}
 		}
-		return repo.InstallApp(*app, "infrastructure", values)
+		return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), values)
 	}
 	appsToInstall := []string{
 		"resource-renderer-controller",
@@ -495,7 +495,7 @@
 			return err
 		}
 	}
-	return repo.InstallApp(*app, "infrastructure", map[string]any{
+	return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), map[string]any{
 		"Global": global,
 		"Values": map[string]any{
 			"RepoIP":        bootstrapFlags.softServeIP,
diff --git a/core/installer/namespace.go b/core/installer/namespace.go
index 87cd8c4..0acc608 100644
--- a/core/installer/namespace.go
+++ b/core/installer/namespace.go
@@ -5,6 +5,43 @@
 	"fmt"
 )
 
+type SuffixGenerator interface {
+	Generate() (string, error)
+}
+
+type emptySuffixGenerator struct {
+}
+
+func NewEmptySuffixGenerator() SuffixGenerator {
+	return &emptySuffixGenerator{}
+}
+
+func (g *emptySuffixGenerator) Generate() (string, error) {
+	return "", nil
+}
+
+type fixedLengthRandomSuffixGenerator struct {
+	len int
+}
+
+func NewFixedLengthRandomSuffixGenerator(len int) SuffixGenerator {
+	return &fixedLengthRandomSuffixGenerator{len}
+}
+
+var letters = []rune("abcdefghijklmnopqrstuvwxyz")
+
+func (g *fixedLengthRandomSuffixGenerator) Generate() (string, error) {
+	r := make([]byte, g.len)
+	if _, err := rand.Read(r); err != nil {
+		return "", err
+	}
+	ret := make([]rune, g.len)
+	for i, v := range r {
+		ret[i] += letters[v%26]
+	}
+	return fmt.Sprintf("-%s", string(ret)), nil
+}
+
 type NamespaceGenerator interface {
 	Generate(name string) (string, error)
 }
@@ -20,45 +57,3 @@
 func (g *prefixGenerator) Generate(name string) (string, error) {
 	return g.prefix + name, nil
 }
-
-type randomSuffixGenerator struct {
-	len int
-}
-
-func NewRandomSuffixGenerator(len int) NamespaceGenerator {
-	return &randomSuffixGenerator{len}
-}
-
-var letters = []rune("abcdefghijklmnopqrstuvwxyz")
-
-func (g randomSuffixGenerator) Generate(name string) (string, error) {
-	r := make([]byte, g.len)
-	if _, err := rand.Read(r); err != nil {
-		return "", err
-	}
-	ret := make([]rune, g.len)
-	for i, v := range r {
-		ret[i] += letters[v%26]
-	}
-	return fmt.Sprintf("%s-%s", name, string(ret)), nil
-}
-
-type combineGenerator struct {
-	ns []NamespaceGenerator
-}
-
-func NewCombine(ns ...NamespaceGenerator) NamespaceGenerator {
-	return &combineGenerator{ns}
-}
-
-func (g *combineGenerator) Generate(name string) (string, error) {
-	cur := name
-	var err error
-	for _, i := range g.ns {
-		cur, err = i.Generate(cur)
-		if err != nil {
-			return "", err
-		}
-	}
-	return cur, nil
-}
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index 4574fee..671bfb8 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -158,19 +158,29 @@
 	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
-		}
+func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any) error {
+	if !filepath.IsAbs(appRootDir) {
+		return fmt.Errorf("Expected absolute path: %s", appRootDir)
 	}
-	appRootDir := path.Join(root, app.Name)
+	appRootDir = filepath.Clean(appRootDir)
+	for p := appRootDir; p != "/"; {
+		parent, child := filepath.Split(p)
+		kustPath := filepath.Join(parent, "kustomization.yaml")
+		kust, err := r.ReadKustomization(kustPath)
+		if err != nil {
+			if errors.Is(err, fs.ErrNotExist) {
+				k := NewKustomization()
+				kust = &k
+			} else {
+				return err
+			}
+		}
+		kust.AddResources(child)
+		if err := r.WriteKustomization(kustPath, *kust); err != nil {
+			return err
+		}
+		p = filepath.Clean(parent)
+	}
 	{
 		if err := r.RemoveDir(appRootDir); err != nil {
 			return err
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 67e51d9..ab1504d 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -181,12 +181,13 @@
 	}
 	r.CommitAndPush("initialize config")
 	nsGen := installer.NewPrefixGenerator(req.Name + "-")
+	suffixGen := installer.NewEmptySuffixGenerator()
 	{
 		app, err := appsRepo.Find("metallb-config-env")
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 			"IngressPrivate": "10.1.0.1",
 			"Headscale":      "10.1.0.2",
 			"SoftServe":      "10.1.0.3",
@@ -203,7 +204,7 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{}); err != nil {
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{}); err != nil {
 			return err
 		}
 	}
@@ -212,7 +213,7 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{}); err != nil {
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{}); err != nil {
 			return err
 		}
 	}
@@ -221,7 +222,7 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
 		}); err != nil {
 			return err
@@ -232,7 +233,7 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 			"Subdomain": "headscale",
 		}); err != nil {
 			return err
@@ -254,7 +255,7 @@
 		if err != nil {
 			return err
 		}
-		if err := appManager.Install(*app, nsGen, map[string]any{
+		if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 			"RepoAddr":      ss.GetRepoAddress(req.Name),
 			"SSHPrivateKey": keys.Private,
 		}); err != nil {
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index d34580f..ac13f06 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -95,7 +95,8 @@
 		if err != nil {
 			return err
 		}
-		nsGen := installer.NewPrefixGenerator(config.Values.Id + "-")
+		nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
+		suffixGen := installer.NewEmptySuffixGenerator()
 		appManager, err := installer.NewAppManager(s.repo, s.nsCreator)
 		if err != nil {
 			return err
@@ -106,7 +107,7 @@
 			if err != nil {
 				return err
 			}
-			if err := appManager.Install(*app, nsGen, map[string]any{
+			if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 				"GandiAPIToken": req.GandiAPIToken,
 			}); err != nil {
 				return err
@@ -117,7 +118,7 @@
 			if err != nil {
 				return err
 			}
-			if err := appManager.Install(*app, nsGen, map[string]any{
+			if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 				"Username": req.Username,
 				"IPSubnet": "10.1.0.0/24", // TODO(giolekva): this should be taken from the config generated during new env creation
 			}); err != nil {