ClusterManager: Implements support of remote clusters.

After this change users will be able to:
* Create cluster and add/remove servers to it
* Install apps on remote cluster
* Move already installed apps between clusters
* Apps running on server being removed will auto-migrate
  to another server from that same cluster

This is achieved by:
* Installing and running minimal version of dodo on remote cluster
* Ingress-nginx is installed automatically on new clusters
* Next to nginx we run VPN client in the same pod, so that
  default cluster can establish secure communication with it
* Multiple reverse proxies are configured to get to the
  remote cluster service from ingress installed on default cluster.

Next steps:
* Support remote clusters in dodo apps (prototype ready)
* Clean up old cluster when moving app to the new one. Currently
  old cluster keeps running app pods even though no ingress can
  reach it anymore.

Change-Id: Iffc908c93416d4126a8e1c2832eae7b659cb8044
diff --git a/core/installer/tasks/install.go b/core/installer/tasks/install.go
index 8b5bef7..6ff939b 100644
--- a/core/installer/tasks/install.go
+++ b/core/installer/tasks/install.go
@@ -1,7 +1,12 @@
 package tasks
 
 import (
+	"fmt"
+	"path/filepath"
+
 	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/cluster"
+	"github.com/giolekva/pcloud/core/installer/soft"
 )
 
 type InstallFunc func() (installer.ReleaseResources, error)
@@ -50,3 +55,144 @@
 	t := newParentTask("Installing application", true, start, d)
 	return &t
 }
+
+func NewClusterInitTask(m cluster.Manager, server cluster.Server, cnc installer.ClusterNetworkConfigurator, repo soft.RepoIO, setupFn cluster.ClusterSetupFunc) Task {
+	d := &dynamicTaskSlice{t: []Task{}}
+	done := make(chan error)
+	setupTask := newLeafTask(fmt.Sprintf("Installing dodo on %s", server.IP.String()), func() error {
+		_, err := m.Init(server, setupFn)
+		return err
+	})
+	d.Append(&setupTask)
+	setupTask.OnDone(func(err error) {
+		if err != nil {
+			done <- err
+			return
+		}
+		if err := cnc.AddCluster(m.State().Name, m.State().IngressIP); err != nil {
+			done <- err
+			return
+		}
+		_, err = repo.Do(func(fs soft.RepoFS) (string, error) {
+			if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", m.State().Name), m.State()); err != nil {
+				return "", err
+			}
+			return fmt.Sprintf("add server to cluster: %s", m.State().Name), nil
+		})
+		done <- err
+	})
+	start := func() error {
+		setupTask.Start()
+		return <-done
+	}
+	t := newParentTask("Installing application", true, start, d)
+	return &t
+}
+
+func NewRemoveClusterTask(m cluster.Manager, cnc installer.ClusterNetworkConfigurator,
+	repo soft.RepoIO) Task {
+	t := newLeafTask(fmt.Sprintf("Removing %s cluster", m.State().Name), func() error {
+		if err := cnc.RemoveCluster(m.State().Name, m.State().IngressIP); err != nil {
+			return err
+		}
+		_, err := repo.Do(func(fs soft.RepoFS) (string, error) {
+			if err := fs.RemoveAll(fmt.Sprintf("/clusters/%s", m.State().Name)); err != nil {
+				return "", err
+			}
+			kustPath := filepath.Join("/clusters", "kustomization.yaml")
+			kust, err := soft.ReadKustomization(fs, kustPath)
+			if err != nil {
+				return "", err
+			}
+			kust.RemoveResources(m.State().Name)
+			soft.WriteYaml(fs, kustPath, kust)
+			return fmt.Sprintf("remove cluster: %s", m.State().Name), nil
+		})
+		return err
+	})
+	return &t
+}
+
+func NewClusterJoinControllerTask(m cluster.Manager, server cluster.Server, repo soft.RepoIO) Task {
+	d := &dynamicTaskSlice{t: []Task{}}
+	done := make(chan error)
+	setupTask := newLeafTask(fmt.Sprintf("Joining %s to %s cluster", server.IP.String(), m.State().Name), func() error {
+		return m.JoinController(server)
+	})
+	d.Append(&setupTask)
+	setupTask.OnDone(func(err error) {
+		if err != nil {
+			done <- err
+			return
+		}
+		_, err = repo.Do(func(fs soft.RepoFS) (string, error) {
+			if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", m.State().Name), m.State()); err != nil {
+				return "", err
+			}
+			return fmt.Sprintf("add controller server to cluster: %s", m.State().Name), nil
+		})
+		done <- err
+	})
+	start := func() error {
+		setupTask.Start()
+		return <-done
+	}
+	t := newParentTask("Installing application", true, start, d)
+	return &t
+}
+
+func NewClusterJoinWorkerTask(m cluster.Manager, server cluster.Server, repo soft.RepoIO) Task {
+	d := &dynamicTaskSlice{t: []Task{}}
+	done := make(chan error)
+	setupTask := newLeafTask(fmt.Sprintf("Joining %s to %s cluster", server.IP.String(), m.State().Name), func() error {
+		return m.JoinWorker(server)
+	})
+	d.Append(&setupTask)
+	setupTask.OnDone(func(err error) {
+		if err != nil {
+			done <- err
+			return
+		}
+		_, err = repo.Do(func(fs soft.RepoFS) (string, error) {
+			if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", m.State().Name), m.State()); err != nil {
+				return "", err
+			}
+			return fmt.Sprintf("add worker server to cluster: %s", m.State().Name), nil
+		})
+		done <- err
+	})
+	start := func() error {
+		setupTask.Start()
+		return <-done
+	}
+	t := newParentTask("Installing application", true, start, d)
+	return &t
+}
+
+func NewClusterRemoveServerTask(m cluster.Manager, server string, repo soft.RepoIO) Task {
+	d := &dynamicTaskSlice{t: []Task{}}
+	done := make(chan error)
+	setupTask := newLeafTask(fmt.Sprintf("Removing %s from %s cluster", server, m.State().Name), func() error {
+		return m.RemoveServer(server)
+	})
+	d.Append(&setupTask)
+	setupTask.OnDone(func(err error) {
+		if err != nil {
+			done <- err
+			return
+		}
+		_, err = repo.Do(func(fs soft.RepoFS) (string, error) {
+			if err := soft.WriteJson(fs, fmt.Sprintf("/clusters/%s/config.json", m.State().Name), m.State()); err != nil {
+				return "", err
+			}
+			return fmt.Sprintf("remove %s from cluster: %s", server, m.State().Name), nil
+		})
+		done <- err
+	})
+	start := func() error {
+		setupTask.Start()
+		return <-done
+	}
+	t := newParentTask("Installing application", true, start, d)
+	return &t
+}