AppManager: Implement task status API

Change-Id: I70c895d7461ffe4afc45868ca6bf754d37072a0f
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index 4013574..7e54a08 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -558,6 +558,10 @@
 						}
 						"\(svc.name)": {
 							chart: charts.app
+							annotations: {
+								"dodo.cloud/resource-type":               "service"
+								"dodo.cloud/resource.service.name":    svc.name
+							}
 							values: {
 								image: {
 									repository: images.app.fullName
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index cb372f3..b26fc85 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -142,6 +142,7 @@
 	r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
 	r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
 	r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
+	r.HandleFunc("/api/tasks/{instanceId}", s.handleTaskStatusAPI).Methods(http.MethodGet)
 	r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
 	r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
 	r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
@@ -178,6 +179,8 @@
 }
 
 func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
+	s.l.Lock()
+	defer s.l.Unlock()
 	var req dodoAppInstallReq
 	// TODO(gio): validate that no internal fields are overridden by request
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -218,10 +221,16 @@
 }
 
 func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
+	s.l.Lock()
+	defer s.l.Unlock()
 	instanceId, ok := mux.Vars(r)["instanceId"]
 	if !ok {
 		http.Error(w, "missing instance id", http.StatusBadRequest)
 	}
+	if _, ok := s.tasks[instanceId]; ok {
+		http.Error(w, "task in progress", http.StatusTooEarly)
+		return
+	}
 	var req dodoAppInstallReq
 	// TODO(gio): validate that no internal fields are overridden by request
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -242,10 +251,23 @@
 	overrides := installer.CueAppData{
 		"app.cue": cfg.Bytes(),
 	}
-	// TODO(gio): return monitoring info
-	if _, err := s.m.Update(instanceId, nil, overrides); err != nil {
+	rr, err := s.m.Update(instanceId, nil, overrides)
+	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 	}
+	t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
+		if err == nil {
+			ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
+			go s.reconciler.Reconcile(ctx)
+		}
+		return rr, err
+	})
+	if _, ok := s.tasks[instanceId]; ok {
+		panic("MUST NOT REACH!")
+	}
+	s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
+	t.OnDone(s.cleanTask(instanceId, 0))
+	go t.Start()
 }
 
 func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
@@ -467,6 +489,7 @@
 	t.OnDone(s.cleanTask(slug, tid))
 	s.tasks[slug] = &taskForward{t, fmt.Sprintf("/instance/%s", slug), tid}
 	go t.Start()
+	fmt.Printf("Created task for %s\n", slug)
 	if _, err := fmt.Fprintf(w, "/tasks/%s", slug); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -665,7 +688,6 @@
 	t, ok := s.tasks[slug]
 	if !ok {
 		http.Error(w, "task not found", http.StatusInternalServerError)
-
 		return
 	}
 	if ok && t.task == nil {
@@ -682,6 +704,48 @@
 	}
 }
 
+type resourceStatus struct {
+	Type   string `json:"type"`
+	Name   string `json:"name"`
+	Status string `json:"status"`
+}
+
+func extractResources(t tasks.Task) []resourceStatus {
+	var ret []resourceStatus
+	if t.Resource() != nil {
+		ret = append(ret, resourceStatus{
+			Type:   t.Resource().Type,
+			Name:   t.Resource().Name,
+			Status: tasks.StatusString(t.Status()),
+		})
+	}
+	for _, st := range t.Subtasks() {
+		ret = append(ret, extractResources(st)...)
+	}
+	return ret
+}
+
+func (s *Server) handleTaskStatusAPI(w http.ResponseWriter, r *http.Request) {
+	s.l.Lock()
+	defer s.l.Unlock()
+	instanceId, ok := mux.Vars(r)["instanceId"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
+	t, ok := s.tasks[instanceId]
+	if !ok {
+		http.Error(w, "task not found", http.StatusInternalServerError)
+		return
+	}
+	if ok && t.task == nil {
+		http.Error(w, "not found", http.StatusNotFound)
+		return
+	}
+	resources := extractResources(t.task)
+	json.NewEncoder(w).Encode(resources)
+}
+
 type clustersData struct {
 	CurrentPage string
 	Clusters    []cluster.State
diff --git a/core/installer/tasks/release.go b/core/installer/tasks/release.go
index 9a99698..059d1ee 100644
--- a/core/installer/tasks/release.go
+++ b/core/installer/tasks/release.go
@@ -19,7 +19,23 @@
 }
 
 func newMonitorHelm(mon installer.HelmReleaseMonitor, h installer.Resource) Task {
-	t := newLeafTask(h.Info, func() error {
+	rType := h.Annotations["dodo.cloud/resource-type"]
+	var name string
+	switch rType {
+	case "virtual-machine":
+		name = h.Annotations["dodo.cloud/resource.virtual-machine.name"]
+	case "mongodb":
+		name = h.Annotations["dodo.cloud/resource.mongodb.name"]
+	case "postgresql":
+		name = h.Annotations["dodo.cloud/resource.postgresql.name"]
+	case "volume":
+		name = h.Annotations["dodo.cloud/resource.volume.name"]
+	case "ingress":
+		name = h.Annotations["dodo.cloud/resource.ingress.host"]
+	case "service":
+		name = h.Annotations["dodo.cloud/resource.service.name"]
+	}
+	t := newResourceLeafTask(h.Info, &ResourceId{rType, name}, func() error {
 		for {
 			if ok, err := mon.IsReleased(h.Namespace, h.Name); err == nil && ok {
 				break
diff --git a/core/installer/tasks/tasks.go b/core/installer/tasks/tasks.go
index 69ddd17..a391e7e 100644
--- a/core/installer/tasks/tasks.go
+++ b/core/installer/tasks/tasks.go
@@ -13,13 +13,33 @@
 	StatusDone           = 3
 )
 
+func StatusString(s Status) string {
+	switch s {
+	case StatusPending:
+		return "pending"
+	case StatusRunning:
+		return "running"
+	case StatusFailed:
+		return "failed"
+	case StatusDone:
+		return "done"
+	}
+	panic("MUST NOT REACH")
+}
+
 type TaskDoneListener func(err error)
 
 type Subtasks interface {
 	Tasks() []Task
 }
 
+type ResourceId struct {
+	Type string
+	Name string
+}
+
 type Task interface {
+	Resource() *ResourceId
 	Title() string
 	Start()
 	Status() Status
@@ -30,6 +50,7 @@
 
 type basicTask struct {
 	title       string
+	rid         *ResourceId
 	status      Status
 	err         error
 	listeners   []TaskDoneListener
@@ -37,8 +58,9 @@
 	afterDone   func()
 }
 
-func newBasicTask(title string) basicTask {
+func newBasicTask(title string, rid *ResourceId) basicTask {
 	return basicTask{
+		rid:       rid,
 		title:     title,
 		status:    StatusPending,
 		err:       nil,
@@ -46,6 +68,10 @@
 	}
 }
 
+func (b *basicTask) Resource() *ResourceId {
+	return b.rid
+}
+
 func (b *basicTask) Title() string {
 	return b.title
 }
@@ -84,7 +110,14 @@
 
 func newLeafTask(title string, start func() error) leafTask {
 	return leafTask{
-		basicTask: newBasicTask(title),
+		basicTask: newBasicTask(title, nil),
+		start:     start,
+	}
+}
+
+func newResourceLeafTask(title string, rid *ResourceId, start func() error) leafTask {
+	return leafTask{
+		basicTask: newBasicTask(title, rid),
 		start:     start,
 	}
 }