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/schema.go b/core/installer/schema.go
index fcdebd4..5a70519 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -23,6 +23,7 @@
 	KindArrayString       = 8
 	KindPort              = 9
 	KindVPNAuthKey        = 11
+	KindCluster           = 12
 )
 
 type Field struct {
@@ -56,6 +57,34 @@
 	advanced: true,
 }
 
+const clusterSchema = `
+#Cluster: {
+    name: string
+	kubeconfig: string
+    ingressClassName: string
+}
+
+value: { %s }
+`
+
+func isCluster(v cue.Value) bool {
+	if v.Value().Kind() != cue.StructKind {
+		return false
+	}
+	s := fmt.Sprintf(clusterSchema, fmt.Sprintf("%#v", v))
+	c := cuecontext.New()
+	u := c.CompileString(s)
+	if err := u.Validate(); err != nil {
+		return false
+	}
+	cluster := u.LookupPath(cue.ParsePath("#Cluster"))
+	vv := u.LookupPath(cue.ParsePath("value"))
+	if err := cluster.Subsume(vv); err == nil {
+		return true
+	}
+	return false
+}
+
 const networkSchema = `
 #Network: {
     name: string
@@ -233,18 +262,18 @@
 			meta := map[string]string{}
 			usernameFieldAttr := v.Attribute("usernameField")
 			if usernameFieldAttr.Err() == nil {
-				meta["usernameField"] = strings.ToLower(usernameFieldAttr.Contents())
+				meta["usernameField"] = usernameFieldAttr.Contents()
 			}
 			usernameAttr := v.Attribute("username")
 			if usernameAttr.Err() == nil {
-				meta["username"] = strings.ToLower(usernameAttr.Contents())
+				meta["username"] = usernameAttr.Contents()
 			}
 			if len(meta) != 1 {
 				return nil, fmt.Errorf("invalid vpn auth key field meta: %+v", meta)
 			}
 			enabledFieldAttr := v.Attribute("enabledField")
 			if enabledFieldAttr.Err() == nil {
-				meta["enabledField"] = strings.ToLower(enabledFieldAttr.Contents())
+				meta["enabledField"] = enabledFieldAttr.Contents()
 			}
 			return basicSchema{name, KindVPNAuthKey, true, meta}, nil
 		} else {
@@ -272,9 +301,11 @@
 			return basicSchema{name, KindAuth, false, nil}, nil
 		} else if isSSHKey(v) {
 			return basicSchema{name, KindSSHKey, true, nil}, nil
+		} else if isCluster(v) {
+			return basicSchema{name, KindCluster, false, nil}, nil
 		}
 		s := structSchema{name, make([]Field, 0), false}
-		f, err := v.Fields(cue.Schema())
+		f, err := v.Fields(cue.All())
 		if err != nil {
 			return nil, err
 		}
@@ -283,10 +314,14 @@
 			if err != nil {
 				return nil, err
 			}
-			s.fields = append(s.fields, Field{f.Selector().String(), scm})
+			s.fields = append(s.fields, Field{cleanFieldName(f.Selector().String()), scm})
 		}
 		return s, nil
 	default:
 		return nil, fmt.Errorf("SHOULD NOT REACH!")
 	}
 }
+
+func cleanFieldName(name string) string {
+	return strings.ReplaceAll(strings.ReplaceAll(name, "?", ""), "!", "")
+}