DodoApp: API to create new app

Change-Id: I20d73ef17cc03073c913fceb4f3bed7a26754cea
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index e032393..f903d9b 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -10,42 +10,58 @@
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
+
+	"github.com/gorilla/mux"
 )
 
 type DodoAppServer struct {
-	port      int
-	sshKey    string
-	client    soft.Client
-	namespace string
-	env       installer.EnvConfig
-	jc        installer.JobCreator
-	workers   map[string]map[string]struct{}
+	port             int
+	self             string
+	sshKey           string
+	gitRepoPublicKey string
+	client           soft.Client
+	namespace        string
+	env              installer.EnvConfig
+	nsc              installer.NamespaceCreator
+	jc               installer.JobCreator
+	workers          map[string]map[string]struct{}
+	appNs            map[string]string
 }
 
+// TODO(gio): Initialize appNs on startup
 func NewDodoAppServer(
 	port int,
+	self string,
 	sshKey string,
+	gitRepoPublicKey string,
 	client soft.Client,
 	namespace string,
+	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
 ) *DodoAppServer {
 	return &DodoAppServer{
 		port,
+		self,
 		sshKey,
+		gitRepoPublicKey,
 		client,
 		namespace,
 		env,
+		nsc,
 		jc,
 		map[string]map[string]struct{}{},
+		map[string]string{},
 	}
 }
 
 func (s *DodoAppServer) Start() error {
-	http.HandleFunc("/update", s.handleUpdate)
-	http.HandleFunc("/register-worker", s.handleRegisterWorker)
-	http.HandleFunc("/api/add-admin-key", s.handleAddAdminKey)
-	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
+	r := mux.NewRouter()
+	r.HandleFunc("/update", s.handleUpdate)
+	r.HandleFunc("/register-worker", s.handleRegisterWorker).Methods(http.MethodPost)
+	r.HandleFunc("/api/apps", s.handleCreateApp).Methods(http.MethodPost)
+	r.HandleFunc("/api/add-admin-key", s.handleAddAdminKey).Methods(http.MethodPost)
+	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 }
 
 type updateReq struct {
@@ -66,12 +82,12 @@
 		fmt.Println(err)
 		return
 	}
-	if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "dodo") {
+	if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "config") {
 		return
 	}
 	go func() {
 		time.Sleep(20 * time.Second)
-		if err := UpdateDodoApp(req.Repository.Name, s.client, s.namespace, s.sshKey, s.jc, &s.env); err != nil {
+		if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
 			fmt.Println(err)
 		}
 	}()
@@ -100,6 +116,138 @@
 	s.workers[req.AppId][req.Address] = struct{}{}
 }
 
+type createAppReq struct {
+	AdminPublicKey string `json:"adminPublicKey"`
+}
+
+type createAppResp struct {
+	AppName string `json:"appName"`
+}
+
+func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
+	var req createAppReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	g := installer.NewFixedLengthRandomNameGenerator(3)
+	appName, err := g.Generate()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := s.CreateApp(appName, req.AdminPublicKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	resp := createAppResp{appName}
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) error {
+	fmt.Printf("Creating app: %s\n", appName)
+	if ok, err := s.client.RepoExists(appName); err != nil {
+		return err
+	} else if ok {
+		return nil
+	}
+	if err := s.client.AddRepository(appName); err != nil {
+		return err
+	}
+	appRepo, err := s.client.GetRepo(appName)
+	if err != nil {
+		return err
+	}
+	if err := InitRepo(appRepo); err != nil {
+		return err
+	}
+	apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	app, err := installer.FindEnvApp(apps, "dodo-app-instance")
+	if err != nil {
+		return err
+	}
+	suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
+	suffix, err := suffixGen.Generate()
+	if err != nil {
+		return err
+	}
+	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
+	s.appNs[appName] = namespace
+	if err := s.updateDodoApp(appName, namespace); err != nil {
+		return err
+	}
+	if ok, err := s.client.RepoExists("config"); err != nil {
+		return err
+	} else if !ok {
+		if err := s.client.AddRepository("config"); err != nil {
+			return err
+		}
+	}
+	repo, err := s.client.GetRepo("config")
+	if err != nil {
+		return err
+	}
+	hf := installer.NewGitHelmFetcher()
+	m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
+	if err != nil {
+		return err
+	}
+	if _, err := m.Install(app, appName, "/"+appName, namespace, map[string]any{
+		"repoAddr":         s.client.GetRepoAddress(appName),
+		"repoHost":         strings.Split(s.client.Address(), ":")[0],
+		"gitRepoPublicKey": s.gitRepoPublicKey,
+	}, installer.WithConfig(&s.env)); err != nil {
+		return err
+	}
+	cfg, err := m.FindInstance(appName)
+	if err != nil {
+		return err
+	}
+	fluxKeys, ok := cfg.Input["fluxKeys"]
+	if !ok {
+		return fmt.Errorf("Fluxcd keys not found")
+	}
+	fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
+	if !ok {
+		return fmt.Errorf("Fluxcd keys not found")
+	}
+	if ok, err := s.client.UserExists("fluxcd"); err != nil {
+		return err
+	} else if ok {
+		if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
+			return err
+		}
+	} else {
+		if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
+			return err
+		}
+	}
+	if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
+		return err
+	}
+	if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
+		return err
+	}
+	if user, err := s.client.FindUser(adminPublicKey); err != nil {
+		return err
+	} else if user != "" {
+		if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
+			return err
+		}
+	} else {
+		if err := s.client.AddUser(appName, adminPublicKey); err != nil {
+			return err
+		}
+		if err := s.client.AddReadWriteCollaborator(appName, appName); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 type addAdminKeyReq struct {
 	Public string `json:"public"`
 }
@@ -116,22 +264,17 @@
 	}
 }
 
-func UpdateDodoApp(name string, client soft.Client, namespace string, sshKey string, jc installer.JobCreator, env *installer.EnvConfig) error {
-	repo, err := client.GetRepo(name)
-	if err != nil {
-		return err
-	}
-	nsc := installer.NewNoOpNamespaceCreator()
+func (s *DodoAppServer) updateDodoApp(name, namespace string) error {
+	repo, err := s.client.GetRepo(name)
 	if err != nil {
 		return err
 	}
 	hf := installer.NewGitHelmFetcher()
-	m, err := installer.NewAppManager(repo, nsc, jc, hf, "/.dodo")
+	m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
 	if err != nil {
 		return err
 	}
 	appCfg, err := soft.ReadFile(repo, "app.cue")
-	fmt.Println(string(appCfg))
 	if err != nil {
 		return err
 	}
@@ -146,11 +289,12 @@
 		"/.dodo/app",
 		namespace,
 		map[string]any{
-			"repoAddr":      repo.FullAddress(),
-			"appId":         name,
-			"sshPrivateKey": sshKey,
+			"repoAddr":           repo.FullAddress(),
+			"registerWorkerAddr": fmt.Sprintf("http://%s/register-worker", s.self),
+			"appId":              name,
+			"sshPrivateKey":      s.sshKey,
 		},
-		installer.WithConfig(env),
+		installer.WithConfig(&s.env),
 		installer.WithLocalChartGenerator(lg),
 		installer.WithBranch("dodo"),
 		installer.WithForce(),
@@ -159,3 +303,71 @@
 	}
 	return nil
 }
+
+const goMod = `module dodo.app
+
+go 1.18
+`
+
+const mainGo = `package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintln(w, "Hello from Dodo App!")
+}
+
+func main() {
+	flag.Parse()
+	http.HandleFunc("/", handler)
+	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
+`
+
+const appCue = `app: {
+	type: "golang:1.22.0"
+	run: "main.go"
+	ingress: {
+		network: "Private" // or Public
+		subdomain: "testapp"
+		auth: enabled: false
+	}
+}
+`
+
+func InitRepo(repo soft.RepoIO) error {
+	return repo.Do(func(fs soft.RepoFS) (string, error) {
+		{
+			w, err := fs.Writer("go.mod")
+			if err != nil {
+				return "", err
+			}
+			defer w.Close()
+			fmt.Fprint(w, goMod)
+		}
+		{
+			w, err := fs.Writer("main.go")
+			if err != nil {
+				return "", err
+			}
+			defer w.Close()
+			fmt.Fprintf(w, "%s", mainGo)
+		}
+		{
+			w, err := fs.Writer("app.cue")
+			if err != nil {
+				return "", err
+			}
+			defer w.Close()
+			fmt.Fprint(w, appCue)
+		}
+		return "go web app template", nil
+	})
+}
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index d3f5d42..4acc576 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -109,6 +109,10 @@
 	return []string{}, nil
 }
 
+func (f fakeSoftServeClient) RepoExists(name string) (bool, error) {
+	return false, nil
+}
+
 func (f fakeSoftServeClient) GetRepo(name string) (soft.RepoIO, error) {
 	var l sync.Mutex
 	return mockRepoIO{soft.NewBillyRepoFS(f.envFS), "foo.bar", f.t, &l}, nil
@@ -122,6 +126,13 @@
 	return nil
 }
 
+func (f fakeSoftServeClient) UserExists(name string) (bool, error) {
+	return false, nil
+}
+func (f fakeSoftServeClient) FindUser(pubKey string) (string, error) {
+	return "", nil
+}
+
 func (f fakeSoftServeClient) AddUser(name, pubKey string) error {
 	return nil
 }