DodoApp: Status page

Implements basic status page, listing all apps and their commit
statuses. Separates web and api endpoints. Unifies API addresses a bit.

Change-Id: I98f9f949a49b60e80e188f7b51ec0e967666e65b
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 8cfa72f..89b7ad2 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -9,7 +9,6 @@
 	"net/http"
 	"strings"
 	"sync"
-	"time"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
@@ -18,13 +17,15 @@
 )
 
 const (
-	configRepoName = "config"
+	ConfigRepoName = "config"
 	namespacesFile = "/namespaces.json"
 )
 
 type DodoAppServer struct {
 	l                sync.Locker
+	st               Store
 	port             int
+	apiPort          int
 	self             string
 	sshKey           string
 	gitRepoPublicKey string
@@ -39,7 +40,9 @@
 
 // TODO(gio): Initialize appNs on startup
 func NewDodoAppServer(
+	st Store,
 	port int,
+	apiPort int,
 	self string,
 	sshKey string,
 	gitRepoPublicKey string,
@@ -49,16 +52,11 @@
 	jc installer.JobCreator,
 	env installer.EnvConfig,
 ) (*DodoAppServer, error) {
-	if ok, err := client.RepoExists(configRepoName); err != nil {
-		return nil, err
-	} else if !ok {
-		if err := client.AddRepository(configRepoName); err != nil {
-			return nil, err
-		}
-	}
 	s := &DodoAppServer{
 		&sync.Mutex{},
+		st,
 		port,
+		apiPort,
 		self,
 		sshKey,
 		gitRepoPublicKey,
@@ -70,7 +68,7 @@
 		map[string]map[string]struct{}{},
 		map[string]string{},
 	}
-	config, err := client.GetRepo(configRepoName)
+	config, err := client.GetRepo(ConfigRepoName)
 	if err != nil {
 		return nil, err
 	}
@@ -87,12 +85,50 @@
 }
 
 func (s *DodoAppServer) Start() error {
-	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)
+	e := make(chan error)
+	go func() {
+		r := mux.NewRouter()
+		r.HandleFunc("/status/{app-name}", s.handleAppStatus).Methods(http.MethodGet)
+		r.HandleFunc("/status", s.handleStatus).Methods(http.MethodGet)
+		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
+	}()
+	go func() {
+		r := mux.NewRouter()
+		r.HandleFunc("/update", s.handleUpdate)
+		r.HandleFunc("/api/apps/{app-name}/workers", 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)
+		e <- http.ListenAndServe(fmt.Sprintf(":%d", s.apiPort), r)
+	}()
+	return <-e
+}
+
+func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
+	apps, err := s.st.GetApps()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	for _, a := range apps {
+		fmt.Fprintf(w, "%s\n", a)
+	}
+}
+
+func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	appName, ok := vars["app-name"]
+	if !ok || appName == "" {
+		http.Error(w, "missing app-name", http.StatusBadRequest)
+		return
+	}
+	commits, err := s.st.GetCommitHistory(appName)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	for _, c := range commits {
+		fmt.Fprintf(w, "%s %s\n", c.Hash, c.Message)
+	}
 }
 
 type updateReq struct {
@@ -100,6 +136,7 @@
 	Repository struct {
 		Name string `json:"name"`
 	} `json:"repository"`
+	After string `json:"after"`
 }
 
 func (s *DodoAppServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
@@ -113,38 +150,49 @@
 		fmt.Println(err)
 		return
 	}
-	if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, configRepoName) {
+	if req.Ref != "refs/heads/master" || req.Repository.Name == ConfigRepoName {
 		return
 	}
+	// TODO(gio): Create commit record on app init as well
 	go func() {
-		time.Sleep(20 * time.Second)
 		if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
-			fmt.Println(err)
+			if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
+				fmt.Printf("Error: %s\n", err.Error())
+				return
+			}
+		}
+		if err := s.st.CreateCommit(req.Repository.Name, req.After, "OK"); err != nil {
+			fmt.Printf("Error: %s\n", err.Error())
+		}
+		for addr, _ := range s.workers[req.Repository.Name] {
+			go func() {
+				// TODO(gio): make port configurable
+				http.Get(fmt.Sprintf("http://%s/update", addr))
+			}()
 		}
 	}()
-	for addr, _ := range s.workers[req.Repository.Name] {
-		go func() {
-			// TODO(gio): make port configurable
-			http.Get(fmt.Sprintf("http://%s:3000/update", addr))
-		}()
-	}
 }
 
 type registerWorkerReq struct {
-	AppId   string `json:"appId"`
 	Address string `json:"address"`
 }
 
 func (s *DodoAppServer) handleRegisterWorker(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	appName, ok := vars["app-name"]
+	if !ok || appName == "" {
+		http.Error(w, "missing app-name", http.StatusBadRequest)
+		return
+	}
 	var req registerWorkerReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if _, ok := s.workers[req.AppId]; !ok {
-		s.workers[req.AppId] = map[string]struct{}{}
+	if _, ok := s.workers[appName]; !ok {
+		s.workers[appName] = map[string]struct{}{}
 	}
-	s.workers[req.AppId][req.Address] = struct{}{}
+	s.workers[appName][req.Address] = struct{}{}
 }
 
 type createAppReq struct {
@@ -187,6 +235,9 @@
 	} else if ok {
 		return nil
 	}
+	if err := s.st.CreateApp(appName); err != nil {
+		return err
+	}
 	if err := s.client.AddRepository(appName); err != nil {
 		return err
 	}
@@ -212,7 +263,7 @@
 	if err := s.updateDodoApp(appName, namespace); err != nil {
 		return err
 	}
-	repo, err := s.client.GetRepo(configRepoName)
+	repo, err := s.client.GetRepo(ConfigRepoName)
 	if err != nil {
 		return err
 	}
@@ -337,10 +388,10 @@
 		"/.dodo/app",
 		namespace,
 		map[string]any{
-			"repoAddr":           repo.FullAddress(),
-			"registerWorkerAddr": fmt.Sprintf("http://%s/register-worker", s.self),
-			"appId":              name,
-			"sshPrivateKey":      s.sshKey,
+			"repoAddr":      repo.FullAddress(),
+			"managerAddr":   fmt.Sprintf("http://%s", s.self),
+			"appId":         name,
+			"sshPrivateKey": s.sshKey,
 		},
 		installer.WithConfig(&s.env),
 		installer.WithLocalChartGenerator(lg),
diff --git a/core/installer/welcome/store.go b/core/installer/welcome/store.go
new file mode 100644
index 0000000..0ae5f4e
--- /dev/null
+++ b/core/installer/welcome/store.go
@@ -0,0 +1,103 @@
+package welcome
+
+import (
+	"database/sql"
+
+	"github.com/giolekva/pcloud/core/installer/soft"
+)
+
+type Commit struct {
+	Hash    string
+	Message string
+}
+
+type Store interface {
+	GetApps() ([]string, error)
+	CreateApp(name string) error
+	CreateCommit(name, hash, message string) error
+	GetCommitHistory(name string) ([]Commit, error)
+}
+
+func NewStore(cf soft.RepoIO, db *sql.DB) (Store, error) {
+	s := &storeImpl{cf, db}
+	if err := s.init(); err != nil {
+		return nil, err
+	}
+	return s, nil
+}
+
+type storeImpl struct {
+	cf soft.RepoIO
+	db *sql.DB
+}
+
+func (s *storeImpl) init() error {
+	_, err := s.db.Exec(`
+		CREATE TABLE IF NOT EXISTS apps (
+			name TEXT PRIMARY KEY
+		);
+		CREATE TABLE IF NOT EXISTS commits (
+			app_name TEXT,
+            hash TEXT,
+            message TEXT
+		);
+	`)
+	return err
+
+}
+
+func (s *storeImpl) CreateApp(name string) error {
+	query := `INSERT INTO apps (name) VALUES (?)`
+	_, err := s.db.Exec(query, name)
+	return err
+}
+
+func (s *storeImpl) GetApps() ([]string, error) {
+	query := `SELECT name FROM apps`
+	rows, err := s.db.Query(query)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	ret := []string{}
+	for rows.Next() {
+		if err := rows.Err(); err != nil {
+			return nil, err
+		}
+		var name string
+		if err := rows.Scan(&name); err != nil {
+			return nil, err
+		}
+		ret = append(ret, name)
+
+	}
+	return ret, nil
+}
+
+func (s *storeImpl) CreateCommit(name, hash, message string) error {
+	query := `INSERT INTO commits (app_name, hash, message) VALUES (?, ?, ?)`
+	_, err := s.db.Exec(query, name, hash, message)
+	return err
+}
+
+func (s *storeImpl) GetCommitHistory(name string) ([]Commit, error) {
+	query := `SELECT hash, message FROM commits WHERE app_name = ?`
+	rows, err := s.db.Query(query, name)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	ret := []Commit{}
+	for rows.Next() {
+		if err := rows.Err(); err != nil {
+			return nil, err
+		}
+		var c Commit
+		if err := rows.Scan(&c.Hash, &c.Message); err != nil {
+			return nil, err
+		}
+		ret = append(ret, c)
+
+	}
+	return ret, nil
+}