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,
}
}