DodoApp: Implement branch and app delete functionalities.

Change-Id: I8bf6ed30a6274203e73e80f05a4b82896509ecb0
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 9b28004..4ed21f4 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -692,13 +692,28 @@
 		}
 		cfg = renderedCfg
 		r.RemoveAll(instanceDir)
-		kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
-		kust, err := soft.ReadKustomization(r, kustPath)
-		if err != nil {
-			return "", err
+		prev := ""
+		curr := instanceDir
+		for prev != curr {
+			p := filepath.Dir(curr)
+			n := filepath.Base(curr)
+			kustPath := filepath.Join(p, "kustomization.yaml")
+			kust, err := soft.ReadKustomization(r, kustPath)
+			if err != nil {
+				return "", err
+			}
+			kust.RemoveResources(n)
+			if len(kust.Resources) > 0 {
+				soft.WriteYaml(r, kustPath, kust)
+				break
+			} else {
+				if err := r.RemoveAll(kustPath); err != nil {
+					return "", err
+				}
+			}
+			prev = curr
+			curr = p
 		}
-		kust.RemoveResources(instanceId)
-		soft.WriteYaml(r, kustPath, kust)
 		return fmt.Sprintf("uninstall: %s", instanceId), nil
 	}); err != nil {
 		return err
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 5163f23..338af24 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -31,6 +31,8 @@
 	RepoExists(name string) (bool, error)
 	GetRepo(name string) (RepoIO, error)
 	GetRepoBranch(name, branch string) (RepoIO, error)
+	DeleteRepoBranch(name, branch string) error
+	DeleteRepo(name string) error
 	GetAllRepos() ([]string, error)
 	GetRepoAddress(name string) string
 	AddRepository(name string) error
@@ -281,6 +283,18 @@
 	return err
 }
 
+func (ss *realClient) DeleteRepoBranch(name, branch string) error {
+	log.Printf("Deleting branch %s %s", name, branch)
+	_, err := ss.RunCommand("repo", "branch", "delete", name, branch)
+	return err
+}
+
+func (ss *realClient) DeleteRepo(name string) error {
+	log.Printf("Deleting repo %s", name)
+	_, err := ss.RunCommand("repo", "delete", name)
+	return err
+}
+
 type Repository struct {
 	*git.Repository
 	Addr RepositoryAddress
diff --git a/core/installer/welcome/dodo-app-tmpl/app_status.html b/core/installer/welcome/dodo-app-tmpl/app_status.html
index 916525e..879d0ec 100644
--- a/core/installer/welcome/dodo-app-tmpl/app_status.html
+++ b/core/installer/welcome/dodo-app-tmpl/app_status.html
@@ -22,4 +22,12 @@
 {{- range .Branches -}}
 <a href="/{{ $.Name }}/branch/{{ . }}">{{ . }}</a><br/>
 {{- end -}}
+{{- if ne .Branch "master" -}}
+<form action="/{{ .Name }}/branch/{{ .Branch }}/delete" method="POST">
+	<button aria-busy="false" type="submit" name="delete">delete branch</button>
+</form>
+{{- end -}}
+<form action="/{{ .Name }}/delete" method="POST">
+	<button aria-busy="false" type="submit" name="delete">delete app</button>
+</form>
 {{- end -}}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 256007e..51f38e9 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -236,7 +236,9 @@
 		r.HandleFunc("/{app-name}/{hash}", s.handleAppCommit).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}/dev-branch/create", s.handleCreateDevBranch).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}/branch/{branch}", s.handleAppStatus).Methods(http.MethodGet)
+		r.HandleFunc("/{app-name}/branch/{branch}/delete", s.handleBranchDelete).Methods(http.MethodPost)
 		r.HandleFunc("/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
+		r.HandleFunc("/{app-name}/delete", s.handleAppDelete).Methods(http.MethodPost)
 		r.HandleFunc("/", s.handleStatus).Methods(http.MethodGet)
 		r.HandleFunc("/", s.handleCreateApp).Methods(http.MethodPost)
 		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
@@ -466,6 +468,7 @@
 type appStatusData struct {
 	Navigation      []navItem
 	Name            string
+	Branch          string
 	GitCloneCommand string
 	Commits         []CommitMeta
 	LastCommit      resourceData
@@ -532,6 +535,7 @@
 			navItem{appName, "/" + appName},
 		},
 		Name:            appName,
+		Branch:          branch,
 		GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
 		Commits:         commits,
 		LastCommit:      lastCommitResources,
@@ -899,7 +903,7 @@
 	}
 	branch := r.FormValue("branch")
 	if branch == "" {
-		http.Error(w, "missing network", http.StatusBadRequest)
+		http.Error(w, "missing branch", http.StatusBadRequest)
 		return
 	}
 	if err := s.createDevBranch(appName, "master", branch, user); err != nil {
@@ -909,6 +913,49 @@
 	http.Redirect(w, r, fmt.Sprintf("/%s/branch/%s", appName, branch), http.StatusSeeOther)
 }
 
+func (s *DodoAppServer) handleBranchDelete(w http.ResponseWriter, r *http.Request) {
+	u := r.Context().Value(userCtx)
+	if u == nil {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+	vars := mux.Vars(r)
+	appName, ok := vars["app-name"]
+	if !ok || appName == "" {
+		http.Error(w, "missing app-name", http.StatusBadRequest)
+		return
+	}
+	branch, ok := vars["branch"]
+	if !ok || branch == "" {
+		http.Error(w, "missing branch", http.StatusBadRequest)
+		return
+	}
+	if err := s.deleteBranch(appName, branch); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
+}
+
+func (s *DodoAppServer) handleAppDelete(w http.ResponseWriter, r *http.Request) {
+	u := r.Context().Value(userCtx)
+	if u == nil {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+	vars := mux.Vars(r)
+	appName, ok := vars["app-name"]
+	if !ok || appName == "" {
+		http.Error(w, "missing app-name", http.StatusBadRequest)
+		return
+	}
+	if err := s.deleteApp(appName); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(w, r, fmt.Sprintf("/%s", appName), http.StatusSeeOther)
+}
+
 type apiCreateAppReq struct {
 	AppType        string `json:"type"`
 	AdminPublicKey string `json:"adminPublicKey"`
@@ -1042,6 +1089,68 @@
 	return s.createAppForBranch(appRepo, appName, toBranch, user, network, map[string][]byte{"app.json": branchCfg})
 }
 
+func (s *DodoAppServer) deleteBranch(appName string, branch string) error {
+	appBranch := fmt.Sprintf("dodo_%s", branch)
+	hf := installer.NewGitHelmFetcher()
+	if err := func() error {
+		repo, err := s.client.GetRepoBranch(appName, appBranch)
+		if err != nil {
+			return err
+		}
+		m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/.dodo")
+		if err != nil {
+			return err
+		}
+		return m.Remove("app")
+	}(); err != nil {
+		return err
+	}
+	configRepo, err := s.client.GetRepo(ConfigRepoName)
+	if err != nil {
+		return err
+	}
+	m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, s.vpnKeyGen, s.cnc, "/")
+	if err != nil {
+		return err
+	}
+	appPath := fmt.Sprintf("%s/%s", appName, branch)
+	if _, err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
+		if err := m.Remove(appPath); err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("Uninstalled app branch: %s %s", appName, branch), nil
+	}); err != nil {
+		return err
+	}
+	if err := s.client.DeleteRepoBranch(appName, appBranch); err != nil {
+		return err
+	}
+	if branch != "master" {
+		return s.client.DeleteRepoBranch(appName, branch)
+	}
+	return nil
+}
+
+func (s *DodoAppServer) deleteApp(appName string) error {
+	configRepo, err := s.client.GetRepo(ConfigRepoName)
+	if err != nil {
+		return err
+	}
+	branches, err := configRepo.ListDir(fmt.Sprintf("/%s", appName))
+	if err != nil {
+		return err
+	}
+	for _, b := range branches {
+		if !b.IsDir() || strings.HasPrefix(b.Name(), "dodo_") {
+			continue
+		}
+		if err := s.deleteBranch(appName, b.Name()); err != nil {
+			return err
+		}
+	}
+	return s.client.DeleteRepo(appName)
+}
+
 func (s *DodoAppServer) createAppForBranch(
 	repo soft.RepoIO,
 	appName string,
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index 439fdf2..448f221 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -190,6 +190,14 @@
 	return nil
 }
 
+func (f fakeSoftServeClient) DeleteRepoBranch(_, _ string) error {
+	return nil
+}
+
+func (f fakeSoftServeClient) DeleteRepo(_ string) error {
+	return nil
+}
+
 type fakeClientGetter struct {
 	t     *testing.T
 	envFS billy.Filesystem