AppManager: App installation status monitoring

Change-Id: I64f4ae0d27892b74f8827a275907cb75da09a758
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index b26fc85..80f076a 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -22,6 +22,7 @@
 	"github.com/giolekva/pcloud/core/installer/cluster"
 	"github.com/giolekva/pcloud/core/installer/server"
 	"github.com/giolekva/pcloud/core/installer/soft"
+	"github.com/giolekva/pcloud/core/installer/status"
 	"github.com/giolekva/pcloud/core/installer/tasks"
 )
 
@@ -38,19 +39,21 @@
 }
 
 type Server struct {
-	l            sync.Locker
-	port         int
-	ssClient     soft.Client
-	repo         soft.RepoIO
-	m            *installer.AppManager
-	r            installer.AppRepository
-	fr           installer.AppRepository
-	reconciler   *tasks.FixedReconciler
-	h            installer.HelmReleaseMonitor
-	cnc          installer.ClusterNetworkConfigurator
-	vpnAPIClient installer.VPNAPIClient
-	tasks        map[string]*taskForward
-	tmpl         tmplts
+	l             sync.Locker
+	port          int
+	ssClient      soft.Client
+	repo          soft.RepoIO
+	m             *installer.AppManager
+	r             installer.AppRepository
+	fr            installer.AppRepository
+	reconciler    *tasks.FixedReconciler
+	h             status.ResourceMonitor
+	im            *status.InstanceMonitor
+	cnc           installer.ClusterNetworkConfigurator
+	vpnAPIClient  installer.VPNAPIClient
+	tasks         map[string]*taskForward
+	tmpl          tmplts
+	idToResources map[string]map[string][]status.Resource
 }
 
 type tmplts struct {
@@ -104,7 +107,8 @@
 	r installer.AppRepository,
 	fr installer.AppRepository,
 	reconciler *tasks.FixedReconciler,
-	h installer.HelmReleaseMonitor,
+	h status.ResourceMonitor,
+	im *status.InstanceMonitor,
 	cnc installer.ClusterNetworkConfigurator,
 	vpnAPIClient installer.VPNAPIClient,
 ) (*Server, error) {
@@ -113,19 +117,21 @@
 		return nil, err
 	}
 	return &Server{
-		l:            &sync.Mutex{},
-		port:         port,
-		ssClient:     ssClient,
-		repo:         repo,
-		m:            m,
-		r:            r,
-		fr:           fr,
-		reconciler:   reconciler,
-		h:            h,
-		cnc:          cnc,
-		vpnAPIClient: vpnAPIClient,
-		tasks:        make(map[string]*taskForward),
-		tmpl:         tmpl,
+		l:             &sync.Mutex{},
+		port:          port,
+		ssClient:      ssClient,
+		repo:          repo,
+		m:             m,
+		r:             r,
+		fr:            fr,
+		reconciler:    reconciler,
+		h:             h,
+		im:            im,
+		cnc:           cnc,
+		vpnAPIClient:  vpnAPIClient,
+		tasks:         make(map[string]*taskForward),
+		tmpl:          tmpl,
+		idToResources: make(map[string]map[string][]status.Resource),
 	}, nil
 }
 
@@ -142,7 +148,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/instance/{instanceId}/status", s.handleInstanceStatusAPI).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)
@@ -207,6 +213,24 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	} else {
+		var toMonitor []status.Resource
+		s.idToResources[instanceId] = map[string][]status.Resource{}
+		for _, r := range rr.Helm {
+			resource := status.Resource{
+				Type: status.ResourceHelmRelease,
+				ResourceRef: status.ResourceRef{
+					Name:      r.Name,
+					Namespace: r.Namespace,
+				},
+			}
+			toMonitor = append(toMonitor, resource)
+			if tmp, ok := s.idToResources[instanceId][r.Id]; ok {
+				s.idToResources[instanceId][r.Id] = append(tmp, resource)
+			} else {
+				s.idToResources[instanceId][r.Id] = []status.Resource{resource}
+			}
+		}
+		s.im.Monitor(instanceId, toMonitor)
 		var cfg dodoAppRendered
 		if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&cfg); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -255,6 +279,24 @@
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 	}
+	var toMonitor []status.Resource
+	s.idToResources[instanceId] = map[string][]status.Resource{}
+	for _, r := range rr.Helm {
+		resource := status.Resource{
+			Type: status.ResourceHelmRelease,
+			ResourceRef: status.ResourceRef{
+				Name:      r.Name,
+				Namespace: r.Namespace,
+			},
+		}
+		toMonitor = append(toMonitor, resource)
+		if tmp, ok := s.idToResources[instanceId][r.Id]; ok {
+			s.idToResources[instanceId][r.Id] = append(tmp, resource)
+		} else {
+			s.idToResources[instanceId][r.Id] = []status.Resource{resource}
+		}
+	}
+	s.im.Monitor(instanceId, toMonitor)
 	t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
 		if err == nil {
 			ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
@@ -725,7 +767,69 @@
 	return ret
 }
 
-func (s *Server) handleTaskStatusAPI(w http.ResponseWriter, r *http.Request) {
+type IdName struct {
+	Id   string
+	Name string
+}
+
+type IdNameMap map[string]IdName
+
+type resourceOuts struct {
+	Outs map[string]struct {
+		PostgreSQL IdNameMap `json:"postgresql"`
+		MongoDB    IdNameMap `json:"mongodb"`
+		Volume     IdNameMap `json:"volume"`
+		Ingress    IdNameMap `json:"ingress"`
+	} `json:"outs"`
+}
+
+type DodoResource struct {
+	Type string
+	Name string
+}
+
+type DodoResourceStatus struct {
+	Type   string `json:"type"`
+	Name   string `json:"name"`
+	Status string `json:"status"`
+}
+
+func orginize(raw []byte) (map[string]DodoResource, error) {
+	var outs resourceOuts
+	if err := json.NewDecoder(bytes.NewReader(raw)).Decode(&outs); err != nil {
+		return nil, err
+	}
+	ret := map[string]DodoResource{}
+	for _, out := range outs.Outs {
+		for _, r := range out.PostgreSQL {
+			ret[r.Id] = DodoResource{
+				Type: "postgresql",
+				Name: r.Name,
+			}
+		}
+		for _, r := range out.MongoDB {
+			ret[r.Id] = DodoResource{
+				Type: "mongodb",
+				Name: r.Name,
+			}
+		}
+		for _, r := range out.Volume {
+			ret[r.Id] = DodoResource{
+				Type: "volume",
+				Name: r.Name,
+			}
+		}
+		for _, r := range out.Ingress {
+			ret[r.Id] = DodoResource{
+				Type: "ingress",
+				Name: r.Name,
+			}
+		}
+	}
+	return ret, nil
+}
+
+func (s *Server) handleInstanceStatusAPI(w http.ResponseWriter, r *http.Request) {
 	s.l.Lock()
 	defer s.l.Unlock()
 	instanceId, ok := mux.Vars(r)["instanceId"]
@@ -733,16 +837,40 @@
 		http.Error(w, "empty slug", http.StatusBadRequest)
 		return
 	}
-	t, ok := s.tasks[instanceId]
-	if !ok {
-		http.Error(w, "task not found", http.StatusInternalServerError)
+	statuses, err := s.im.Get(instanceId)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if ok && t.task == nil {
-		http.Error(w, "not found", http.StatusNotFound)
+	idStatus := map[string]status.Status{}
+	for id, resources := range s.idToResources[instanceId] {
+		st := status.StatusNoStatus
+		for _, resource := range resources {
+			if st < statuses[resource] {
+				st = statuses[resource]
+			}
+		}
+		idStatus[id] = st
+	}
+	s.repo.Pull()
+	rendered, err := s.m.AppRendered(instanceId)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	resources := extractResources(t.task)
+	idToResource, err := orginize(rendered)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	resources := []DodoResourceStatus{}
+	for id, st := range idStatus {
+		resources = append(resources, DodoResourceStatus{
+			Type:   idToResource[id].Type,
+			Name:   idToResource[id].Name,
+			Status: status.StatusString(st),
+		})
+	}
 	json.NewEncoder(w).Encode(resources)
 }