AppManager: Run installation in background

Separates process into two sequential tasks: commit to config repo and
monitor release resources.

Change-Id: Ib208839dffc475b5d9c5d21758bc2a18a7f76cb7
diff --git a/core/installer/tasks/install.go b/core/installer/tasks/install.go
new file mode 100644
index 0000000..8b5bef7
--- /dev/null
+++ b/core/installer/tasks/install.go
@@ -0,0 +1,52 @@
+package tasks
+
+import (
+	"github.com/giolekva/pcloud/core/installer"
+)
+
+type InstallFunc func() (installer.ReleaseResources, error)
+
+type dynamicTaskSlice struct {
+	t []Task
+}
+
+func (d *dynamicTaskSlice) Tasks() []Task {
+	return d.t
+}
+
+func (d *dynamicTaskSlice) Append(t Task) {
+	d.t = append(d.t, t)
+}
+
+func NewInstallTask(mon installer.HelmReleaseMonitor, fn InstallFunc) Task {
+	d := &dynamicTaskSlice{t: []Task{}}
+	var rr installer.ReleaseResources
+	done := make(chan error)
+	installTask := newLeafTask("Downloading configuration files", func() error {
+		var err error
+		rr, err = fn()
+		return err
+	})
+	d.Append(&installTask)
+	installTask.OnDone(func(err error) {
+		if err != nil {
+			done <- err
+			return
+		}
+		monTasks := NewMonitorReleaseTasks(mon, rr)
+		for _, mt := range monTasks {
+			d.Append(mt)
+		}
+		monitor := newConcurrentParentTask("Monitor", true, monTasks...)
+		monitor.OnDone(func(err error) {
+			done <- err
+		})
+		monitor.Start()
+	})
+	start := func() error {
+		installTask.Start()
+		return <-done
+	}
+	t := newParentTask("Installing application", true, start, d)
+	return &t
+}
diff --git a/core/installer/tasks/release.go b/core/installer/tasks/release.go
index 229d76e..9a99698 100644
--- a/core/installer/tasks/release.go
+++ b/core/installer/tasks/release.go
@@ -6,12 +6,16 @@
 	"github.com/giolekva/pcloud/core/installer"
 )
 
-func NewMonitorRelease(mon installer.HelmReleaseMonitor, rr installer.ReleaseResources) Task {
+func NewMonitorReleaseTasks(mon installer.HelmReleaseMonitor, rr installer.ReleaseResources) []Task {
 	var t []Task
 	for _, h := range rr.Helm {
 		t = append(t, newMonitorHelm(mon, h))
 	}
-	return newConcurrentParentTask("Monitor", true, t...)
+	return t
+}
+
+func NewMonitorRelease(mon installer.HelmReleaseMonitor, rr installer.ReleaseResources) Task {
+	return newConcurrentParentTask("Monitor", true, NewMonitorReleaseTasks(mon, rr)...)
 }
 
 func newMonitorHelm(mon installer.HelmReleaseMonitor, h installer.Resource) Task {
diff --git a/core/installer/tasks/tasks.go b/core/installer/tasks/tasks.go
index 3db7042..69ddd17 100644
--- a/core/installer/tasks/tasks.go
+++ b/core/installer/tasks/tasks.go
@@ -15,6 +15,10 @@
 
 type TaskDoneListener func(err error)
 
+type Subtasks interface {
+	Tasks() []Task
+}
+
 type Task interface {
 	Title() string
 	Start()
@@ -103,11 +107,17 @@
 
 type parentTask struct {
 	leafTask
-	subtasks     []Task
+	subtasks     Subtasks
 	showChildren bool
 }
 
-func newParentTask(title string, showChildren bool, start func() error, subtasks ...Task) parentTask {
+type TaskSlice []Task
+
+func (s TaskSlice) Tasks() []Task {
+	return s
+}
+
+func newParentTask(title string, showChildren bool, start func() error, subtasks Subtasks) parentTask {
 	return parentTask{
 		leafTask:     newLeafTask(title, start),
 		subtasks:     subtasks,
@@ -117,17 +127,13 @@
 
 func (t *parentTask) Subtasks() []Task {
 	if t.showChildren {
-		return t.subtasks
+		return t.subtasks.Tasks()
 	} else {
 		return make([]Task, 0)
 	}
 }
 
-type sequentialParentTask struct {
-	parentTask
-}
-
-func newSequentialParentTask(title string, showChildren bool, subtasks ...Task) *sequentialParentTask {
+func newSequentialParentTask(title string, showChildren bool, subtasks ...Task) *parentTask {
 	start := func() error {
 		errCh := make(chan error)
 		for i := range subtasks[:len(subtasks)-1] {
@@ -146,16 +152,11 @@
 		go subtasks[0].Start()
 		return <-errCh
 	}
-	return &sequentialParentTask{
-		parentTask: newParentTask(title, showChildren, start, subtasks...),
-	}
+	t := newParentTask(title, showChildren, start, TaskSlice(subtasks))
+	return &t
 }
 
-type concurrentParentTask struct {
-	parentTask
-}
-
-func newConcurrentParentTask(title string, showChildren bool, subtasks ...Task) *concurrentParentTask {
+func newConcurrentParentTask(title string, showChildren bool, subtasks ...Task) *parentTask {
 	start := func() error {
 		errCh := make(chan error)
 		for i := range subtasks {
@@ -177,7 +178,6 @@
 		}
 		return nil
 	}
-	return &concurrentParentTask{
-		parentTask: newParentTask(title, showChildren, start, subtasks...),
-	}
+	t := newParentTask(title, showChildren, start, TaskSlice(subtasks))
+	return &t
 }
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index e6fe096..b25f5b1 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -120,9 +120,12 @@
 {{ end }}
 
 {{ define "extra_menu" }}
-  <li><a href="/app/{{ .App.Slug }}" class="{{ if eq $.CurrentPage .App.Name }}primary{{ end }}">{{ .App.Name }}</a></li>
+  <li><a href="/app/{{ .App.Slug }}" {{ if eq $.CurrentPage .App.Name }}class="primary"{{ end }}>{{ .App.Name }}</a></li>
+  {{ if (and (not $.Instance) $.Task) }}
+  <li><a href="/instance/{{ $.CurrentPage }}" class="primary">{{ $.CurrentPage }}</a></li>
+  {{ end }}
   {{ range .Instances }}
-  <li><a href="/instance/{{ .Id }}" class="{{ if eq $.CurrentPage .Id }}primary{{ end }}">{{ .Id }}</a></li>
+  <li><a href="/instance/{{ .Id }}" {{ if eq $.CurrentPage .Id }}class="primary"{{ end }}>{{ .Id }}</a></li>
   {{ end }}
 {{ end }}
 
@@ -135,7 +138,7 @@
   {{ if .Task }}
     {{if or (eq .Task.Status 0) (eq .Task.Status 1) }}
     {{ $renderForm = false }}
-    Waiting for resources:
+    Installation in progress (feel free to navigate away from this page):
     <ul class="progress">
       {{ template "task" .Task.Subtasks }}
     </ul>
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 29d19ed..345cbab 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -28,6 +28,7 @@
 	reconciler tasks.Reconciler
 	h          installer.HelmReleaseMonitor
 	tasks      map[string]tasks.Task
+	ta         map[string]installer.EnvApp
 	tmpl       tmplts
 }
 
@@ -77,6 +78,7 @@
 		reconciler: reconciler,
 		h:          h,
 		tasks:      make(map[string]tasks.Task),
+		ta:         make(map[string]installer.EnvApp),
 		tmpl:       tmpl,
 	}, nil
 }
@@ -236,21 +238,20 @@
 	instanceId := a.Slug() + suffix
 	appDir := fmt.Sprintf("/apps/%s", instanceId)
 	namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
-	rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
+	t := tasks.NewInstallTask(s.h, func() (installer.ReleaseResources, error) {
+		return s.m.Install(a, instanceId, appDir, namespace, values)
+	})
 	ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
 	go s.reconciler.Reconcile(ctx)
 	if _, ok := s.tasks[instanceId]; ok {
 		panic("MUST NOT REACH!")
 	}
-	t := tasks.NewMonitorRelease(s.h, rr)
+	s.tasks[instanceId] = t
+	s.ta[instanceId] = a
 	t.OnDone(func(err error) {
 		delete(s.tasks, instanceId)
+		delete(s.ta, instanceId)
 	})
-	s.tasks[instanceId] = t
 	go t.Start()
 	if _, err := fmt.Fprintf(w, "/instance/%s", instanceId); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -422,15 +423,25 @@
 		http.Error(w, "empty slug", http.StatusBadRequest)
 		return
 	}
+	t, ok := s.tasks[slug]
 	instance, err := s.m.FindInstance(slug)
-	if err != nil {
+	if err != nil && !ok {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	a, err := s.m.GetInstanceApp(instance.Id)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+	var a installer.EnvApp
+	if instance != nil {
+		a, err = s.m.GetInstanceApp(instance.Id)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	} else {
+		var ok bool
+		a, ok = s.ta[slug]
+		if !ok {
+			panic("MUST NOT REACH!")
+		}
 	}
 	instances, err := s.m.FindAllAppInstances(a.Slug())
 	if err != nil {
@@ -442,14 +453,13 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	t := s.tasks[slug]
 	data := appPageData{
 		App:               a,
 		Instance:          instance,
 		Instances:         instances,
 		AvailableNetworks: networks,
 		Task:              t,
-		CurrentPage:       instance.Id,
+		CurrentPage:       slug,
 	}
 	if err := s.tmpl.app.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)