AppManager: Implement task status API

Change-Id: I70c895d7461ffe4afc45868ca6bf754d37072a0f
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