Installer: Clean up RepoIO interface

Change-Id: If80d7be1460c725b7df9d1d27c9354cb9141acfe
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 295c8ff..e438a26 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -1,8 +1,14 @@
 package installer
 
 import (
+	"bytes"
+	"encoding/json"
+	"errors"
 	"fmt"
+	"io/fs"
 	"io/ioutil"
+	"net/http"
+	"path"
 	"path/filepath"
 
 	"sigs.k8s.io/yaml"
@@ -25,15 +31,58 @@
 }
 
 func (m *AppManager) Config() (Config, error) {
-	return m.repoIO.ReadConfig()
+	var cfg Config
+	if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
+		return Config{}, err
+	} else {
+		return cfg, nil
+	}
+}
+
+func (m *AppManager) appConfig(path string) (AppConfig, error) {
+	var cfg AppConfig
+	if err := ReadYaml(m.repoIO, path, &cfg); err != nil {
+		return AppConfig{}, err
+	} else {
+		return cfg, nil
+	}
 }
 
 func (m *AppManager) FindAllInstances(name string) ([]AppConfig, error) {
-	return m.repoIO.FindAllInstances(appDir, name)
+	kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
+	if err != nil {
+		return nil, err
+	}
+	ret := make([]AppConfig, 0)
+	for _, app := range kust.Resources {
+		cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
+		if err != nil {
+			return nil, err
+		}
+		cfg.Id = app
+		if cfg.AppId == name {
+			ret = append(ret, cfg)
+		}
+	}
+	return ret, nil
 }
 
 func (m *AppManager) FindInstance(id string) (AppConfig, error) {
-	return m.repoIO.FindInstance(appDir, id)
+	kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
+	if err != nil {
+		return AppConfig{}, err
+	}
+	for _, app := range kust.Resources {
+		if app == id {
+			cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
+			if err != nil {
+				return AppConfig{}, err
+			}
+			cfg.Id = id
+			return cfg, nil
+		}
+	}
+	return AppConfig{}, nil
 }
 
 func (m *AppManager) AppConfig(name string) (AppConfig, error) {
@@ -51,58 +100,146 @@
 	return cfg, err
 }
 
-func (m *AppManager) Install(app App, ns NamespaceGenerator, suffixGen SuffixGenerator, config map[string]any) error {
+type allocatePortReq struct {
+	Protocol      string `json:"protocol"`
+	SourcePort    int    `json:"sourcePort"`
+	TargetService string `json:"targetService"`
+	TargetPort    int    `json:"targetPort"`
+}
+
+func openPorts(ports []PortForward) error {
+	for _, p := range ports {
+		var buf bytes.Buffer
+		req := allocatePortReq{
+			Protocol:      p.Protocol,
+			SourcePort:    p.SourcePort,
+			TargetService: p.TargetService,
+			TargetPort:    p.TargetPort,
+		}
+		if err := json.NewEncoder(&buf).Encode(req); err != nil {
+			return err
+		}
+		resp, err := http.Post(p.Allocator, "application/json", &buf)
+		if err != nil {
+			return err
+		}
+		if resp.StatusCode != http.StatusOK {
+			return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
+		}
+	}
+	return nil
+}
+
+func createKustomizationChain(r RepoFS, path string) error {
+	for p := filepath.Clean(path); p != "/"; {
+		parent, child := filepath.Split(p)
+		kustPath := filepath.Join(parent, "kustomization.yaml")
+		kust, err := ReadKustomization(r, kustPath)
+		if err != nil {
+			if errors.Is(err, fs.ErrNotExist) {
+				k := NewKustomization()
+				kust = &k
+			} else {
+				return err
+			}
+		}
+		kust.AddResources(child)
+		if err := WriteYaml(r, kustPath, kust); err != nil {
+			return err
+		}
+		p = filepath.Clean(parent)
+	}
+	return nil
+}
+
+func InstallApp(repo RepoIO, nsc NamespaceCreator, app App, appDir string, namespace string, initValues map[string]any, derived Derived) error {
+	if err := nsc.Create(namespace); err != nil {
+		return err
+	}
+	derived.Release = Release{
+		Namespace: namespace,
+		RepoAddr:  repo.FullAddress(),
+		AppDir:    appDir,
+	}
+	rendered, err := app.Render(derived)
+	if err != nil {
+		return err
+	}
+	if err := openPorts(rendered.Ports); err != nil {
+		return err
+	}
+	return repo.Atomic(func(r RepoFS) (string, error) {
+		if err := createKustomizationChain(r, appDir); err != nil {
+			return "", err
+		}
+		{
+			if err := r.RemoveDir(appDir); err != nil {
+				return "", err
+			}
+			if err := r.CreateDir(appDir); err != nil {
+				return "", err
+			}
+			cfg := AppConfig{
+				AppId:   app.Name(),
+				Config:  initValues,
+				Derived: derived,
+			}
+			if err := WriteYaml(r, path.Join(appDir, configFileName), cfg); err != nil {
+				return "", err
+			}
+		}
+		{
+			appKust := NewKustomization()
+			for name, contents := range rendered.Resources {
+				appKust.AddResources(name)
+				out, err := r.Writer(path.Join(appDir, name))
+				if err != nil {
+					return "", err
+				}
+				defer out.Close()
+				if _, err := out.Write(contents); err != nil {
+					return "", err
+				}
+			}
+			if err := WriteYaml(r, path.Join(appDir, "kustomization.yaml"), appKust); err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("install: %s", app.Name()), nil
+	})
+}
+
+func (m *AppManager) Install(app App, appDir string, namespace string, values map[string]any) error {
+	appDir = filepath.Clean(appDir)
 	if err := m.repoIO.Pull(); err != nil {
 		return err
 	}
-	suffix, err := suffixGen.Generate()
+	globalConfig, err := m.Config()
 	if err != nil {
 		return err
 	}
-	nms, err := ns.Generate(app.Namespace())
+	derivedValues, err := deriveValues(values, app.Schema(), CreateNetworks(globalConfig))
 	if err != nil {
 		return err
 	}
-	nms = nms + suffix
-	if err := m.nsCreator.Create(nms); err != nil {
-		return err
-	}
-	globalConfig, err := m.repoIO.ReadConfig()
-	if err != nil {
-		return err
-	}
-	derivedValues, err := deriveValues(config, app.Schema(), CreateNetworks(globalConfig))
-	if err != nil {
-		fmt.Println(err)
-		return err
-	}
 	derived := Derived{
 		Global: globalConfig.Values,
 		Values: derivedValues,
 	}
-	derived.Release.Namespace = nms
-	fmt.Printf("%+v\n", derived)
-	err = m.repoIO.InstallApp(
-		app,
-		filepath.Join(appDir, app.Name()+suffix),
-		config,
-		derived,
-	)
-	fmt.Println(err)
-	return err
+	return InstallApp(m.repoIO, m.nsCreator, app, appDir, namespace, values, derived)
 }
 
 func (m *AppManager) Update(app App, instanceId string, config map[string]any) error {
 	if err := m.repoIO.Pull(); err != nil {
 		return err
 	}
-	globalConfig, err := m.repoIO.ReadConfig()
+	globalConfig, err := m.Config()
 	if err != nil {
 		return err
 	}
 	instanceDir := filepath.Join(appDir, instanceId)
 	instanceConfigPath := filepath.Join(instanceDir, configFileName)
-	appConfig, err := m.repoIO.ReadAppConfig(instanceConfigPath)
+	appConfig, err := m.appConfig(instanceConfigPath)
 	if err != nil {
 		return err
 	}
@@ -115,14 +252,24 @@
 		Release: appConfig.Derived.Release,
 		Values:  derivedValues,
 	}
-	return m.repoIO.InstallApp(app, instanceDir, config, derived)
+	return InstallApp(m.repoIO, m.nsCreator, app, instanceDir, appConfig.Derived.Release.Namespace, config, derived)
 }
 
 func (m *AppManager) Remove(instanceId string) error {
 	if err := m.repoIO.Pull(); err != nil {
 		return err
 	}
-	return m.repoIO.RemoveApp(filepath.Join(appDir, instanceId))
+	return m.repoIO.Atomic(func(r RepoFS) (string, error) {
+		r.RemoveDir(filepath.Join(appDir, instanceId))
+		kustPath := filepath.Join(appDir, "kustomization.yaml")
+		kust, err := ReadKustomization(r, kustPath)
+		if err != nil {
+			return "", err
+		}
+		kust.RemoveResources(instanceId)
+		WriteYaml(r, kustPath, kust)
+		return fmt.Sprintf("uninstall: %s", instanceId), nil
+	})
 }
 
 // TODO(gio): deduplicate with cue definition in app.go, this one should be removed.