DodoApp: Output access points

Change-Id: I078adfd43bd254e260bf63113a2fb3ab059c7706
diff --git a/core/installer/app.go b/core/installer/app.go
index 0b59ca1..a971653 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -31,6 +31,109 @@
 //go:embed app_configs/app_global_infra.cue
 var cueInfraAppGlobal []byte
 
+type Access struct {
+	Type       string `json:"type"`
+	Name       string `json:"name"`
+	HTTPS      *AccessHTTPS
+	SSH        *AccessSSH
+	TCP        *AccessTCP
+	UDP        *AccessUDP
+	PostgreSQL *AccessPostgreSQL
+	MongoDB    *AccessMongoDB
+}
+
+func (a Access) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+	switch a.Type {
+	case "https":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessHTTPS
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.HTTPS, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	case "ssh":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessSSH
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.SSH, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	case "tcp":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessTCP
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.TCP, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	case "udp":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessUDP
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.UDP, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	case "postgresql":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessPostgreSQL
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.PostgreSQL, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	case "mongodb":
+		if err := json.NewEncoder(&buf).Encode(struct {
+			AccessMongoDB
+			Type string `json:"type"`
+			Name string `json:"name"`
+		}{*a.MongoDB, a.Type, a.Name}); err != nil {
+			return nil, err
+		}
+	default:
+		panic("MUST NOT REACH!")
+	}
+	return buf.Bytes(), nil
+}
+
+type AccessHTTPS struct {
+	Address string `json:"address"`
+}
+
+type AccessSSH struct {
+	Host string `json:"host"`
+	Port int    `json:"port"`
+}
+
+type AccessTCP struct {
+	Host string `json:"host"`
+	Port int    `json:"port"`
+}
+
+type AccessUDP struct {
+	Host string `json:"host"`
+	Port int    `json:"port"`
+}
+
+type AccessPostgreSQL struct {
+	Host     string `json:"host"`
+	Port     int    `json:"port"`
+	Database string `json:"database"`
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
+type AccessMongoDB struct {
+	Host     string `json:"host"`
+	Port     int    `json:"port"`
+	Database string `json:"database"`
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
 type rendered struct {
 	Name            string
 	Readme          string
@@ -45,6 +148,7 @@
 	URL             string
 	Help            []HelpDocument
 	Icon            string
+	Access          []Access
 	Raw             []byte
 }
 
@@ -449,6 +553,122 @@
 		return rendered{}, err
 	}
 	ret.Icon = icon
+	access, err := extractAccess(res.LookupPath(cue.ParsePath("outs")))
+	if err != nil {
+		return rendered{}, err
+	}
+	ret.Access = access
+	return ret, nil
+}
+
+func extractAccessInternal(v cue.Value) ([]Access, error) {
+	ret := []Access{}
+	a := v.LookupPath(cue.ParsePath("access"))
+	if err := a.Err(); err != nil {
+		return nil, err
+	}
+	i, err := a.List()
+	if err != nil {
+		return nil, err
+	}
+	for i.Next() {
+		n := i.Value().LookupPath(cue.ParsePath("name"))
+		if err := n.Err(); err != nil {
+			return nil, err
+		}
+		nn, err := n.String()
+		if err != nil {
+			return nil, err
+		}
+		t := i.Value().LookupPath(cue.ParsePath("type"))
+		if err := t.Err(); err != nil {
+			return nil, err
+		}
+		d, err := t.String()
+		if err != nil {
+			return nil, err
+		}
+		switch d {
+		case "https":
+			{
+				var q AccessHTTPS
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "https", Name: nn, HTTPS: &q})
+			}
+		case "ssh":
+			{
+				var q AccessSSH
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "ssh", Name: nn, SSH: &q})
+			}
+		case "tcp":
+			{
+				var q AccessTCP
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "tcp", Name: nn, TCP: &q})
+			}
+		case "udp":
+			{
+				var q AccessUDP
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "udp", Name: nn, UDP: &q})
+			}
+		case "postgresql":
+			{
+				var q AccessPostgreSQL
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "postgresql", Name: nn, PostgreSQL: &q})
+			}
+		case "mongodb":
+			{
+				var q AccessMongoDB
+				if err := i.Value().Decode(&q); err != nil {
+					return nil, err
+				}
+				ret = append(ret, Access{Type: "mongodb", Name: nn, MongoDB: &q})
+			}
+		}
+	}
+	for _, sub := range []string{"ingress", "postgresql", "mongodb", "services", "vm"} {
+		subout := v.LookupPath(cue.ParsePath(sub))
+		if subout.Err() != nil {
+			continue
+		}
+		if a, err := extractAccess(subout); err != nil {
+			return nil, err
+		} else {
+			ret = append(ret, a...)
+		}
+	}
+	return ret, nil
+}
+
+func extractAccess(v cue.Value) ([]Access, error) {
+	if err := v.Err(); err != nil {
+		return nil, err
+	}
+	i, err := v.Fields()
+	if err != nil {
+		return nil, err
+	}
+	ret := []Access{}
+	for i.Next() {
+		if a, err := extractAccessInternal(i.Value()); err != nil {
+			return nil, fmt.Errorf(errors.Details(err, nil))
+		} else {
+			ret = append(ret, a...)
+		}
+	}
 	return ret, nil
 }
 
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index d4ee363..c189f54 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -337,6 +337,12 @@
 
 	if ssh.enabled {
 		if ssh.expose != null {
+			access: [{
+				name: _name
+				type: "ssh"
+				host: ssh.expose.network.domain
+				port: input[ssh.expose.input_port_name]
+			}]
 			openPort: [{
 				name:    "\(_name)-ssh"
 				network: ssh.expose.network
@@ -515,6 +521,15 @@
 	port: {name: string} | {value: #PortValue}
 }
 
+#MongoDBAccess: #Access & {
+	type:     "mongodb"
+	host:     string
+	port:     number
+	database: string
+	username: string
+	password: string
+}
+
 #MongoDBInput: {
 	cluster?: #Cluster
 	name:     string
@@ -539,6 +554,19 @@
 	_volumeClaimName: "\(name)-mongodb"
 	_initdbScripts:   initdbScripts
 
+	access: [...#MongoDBAccess]
+	access: [
+		for i in openPort {
+			name:     _name
+			type:     "mongodb"
+			host:     i.network.domain
+			port:     i.port
+			database: "mongo"
+			username: "mongo"
+			password: "mongo"
+		},
+	]
+
 	openPort: [for i, e in expose {
 		name:     "port_mongodb_\(_name)_\(i)"
 		network:  networks[strings.ToLower(e.network)]
@@ -687,6 +715,21 @@
 	...
 }
 
+#Access: {
+	name: string
+	type: string
+	...
+}
+
+#PostgreSQLAccess: #Access & {
+	type:     "postgresql"
+	host:     string
+	port:     number
+	database: string
+	username: string
+	password: string
+}
+
 #PostgreSQL: #WithOut & #PostgreSQLInput & {
 	cluster?: #Cluster
 	name:     string
@@ -700,6 +743,19 @@
 	_volumeClaimName: "\(name)-postgresql"
 	_name:            name
 
+	access: [...#PostgreSQLAccess]
+	access: [
+		for i in openPort {
+			name:     _name
+			type:     "postgresql"
+			host:     i.network.domain
+			port:     i.port
+			database: "postgres"
+			username: "postgres"
+			password: "postgres"
+		},
+	]
+
 	openPort: [for i, e in expose {
 		name:     "port_postgresql_\(_name)_\(i)"
 		network:  networks[strings.ToLower(e.network)]
@@ -920,6 +976,24 @@
 
 url: string | *""
 
+#SSHAccess: #Access & {
+	type: "ssh"
+	host: string
+	port: number
+}
+
+#TCPAccess: #Access & {
+	type: "tcp"
+	host: string
+	port: number
+}
+
+#UDPAccess: #Access & {
+	type: "udp"
+	host: string
+	port: number
+}
+
 #WithOut: {
 	cluster?: #Cluster
 	if input.cluster != _|_ {
@@ -932,6 +1006,7 @@
 	images: {...}
 	charts: {...}
 	helm: {...}
+	access: [...#Access] | *[]
 	openPort: [...#PortForward] | *[]
 	openPortMap: {
 		"_self": list.FlattenN([for i, e in openPort {
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index e9e5332..25211a8 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -31,6 +31,11 @@
 
 networks: #Networks
 
+#IngressAccess: #Access & {
+	type:    "https"
+	address: string
+}
+
 #Ingress: #WithOut & {
 	name:      string
 	auth:      #Auth
@@ -47,6 +52,18 @@
 	_cluster: cluster
 	g?:       #Global
 
+	_name: name
+
+	access: [...#IngressAccess]
+
+	if _cluster == _|_ {
+		access: [{
+			name:    _name
+			type:    "https"
+			address: "https://\(subdomain).\(network.domain)"
+		}]
+	}
+
 	_domain:                "\(subdomain).\(network.domain)"
 	_appRoot:               appRoot
 	_authProxyName:         "\(name)-auth-proxy"
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index 810bcf5..50641e2 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -627,6 +627,7 @@
 }
 
 // TODO(gio): Support remote clusters.
+// TODO(gio): pass all svc ports and not only ssh and code-server
 #ServiceDevEnabled: #WithOut & {
 	cluster?: #Cluster
 	_cc:      #Cluster | null | *null
@@ -736,6 +737,20 @@
 	...
 }
 
+#Service: {
+	svc: name: string
+	access: [...(#TCPAccess | #UDPAccess)]
+	access: [
+		for i in openPort {
+			name: svc.name
+			type: strings.ToLower(i.protocol)
+			host: i.network.domain
+			port: i.port
+		},
+	]
+	openPort: [...#PortForward]
+}
+
 #WithOut: {
 	cluster?: #Cluster
 	_cc:      #Cluster | null | *null
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index d0faed4..c98be4b 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -222,6 +222,7 @@
 }
 
 func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
+	fmt.Printf("-- %+v\n-- %+v\n--%+v\n", ports, reservations, allocators)
 	for _, p := range ports {
 		var target string
 		if p.Cluster == "" {
@@ -333,6 +334,7 @@
 type ReleaseResources struct {
 	Release     Release
 	Helm        []Resource
+	Access      []Access
 	RenderedRaw []byte
 }
 
@@ -575,6 +577,7 @@
 	return ReleaseResources{
 		Release:     rendered.Config.Release,
 		RenderedRaw: rendered.Raw,
+		Access:      rendered.Access,
 		Helm:        extractHelm(rendered.Resources),
 	}, nil
 }
@@ -662,6 +665,7 @@
 			portFields = append(portFields, np)
 		}
 	}
+	fmt.Printf("%+v %+v %+v\n", oldPorts, newPorts, portFields)
 	fakeReservations := map[string]reservePortResp{}
 	for i, f := range portFields {
 		fakeReservations[f] = reservePortResp{Port: i}
@@ -784,6 +788,7 @@
 	return ReleaseResources{
 		Release:     rendered.Config.Release,
 		RenderedRaw: rendered.Raw,
+		Access:      rendered.Access,
 		Helm:        extractHelm(rendered.Resources),
 	}, nil
 }
diff --git a/core/installer/dodo_app_test.go b/core/installer/dodo_app_test.go
index cc92d6b..b2cca5d 100644
--- a/core/installer/dodo_app_test.go
+++ b/core/installer/dodo_app_test.go
@@ -562,6 +562,15 @@
           }
         }
       ],
+      "expose": [
+        {
+          "network": "Private",
+          "subdomain": "blog",
+          "port": {
+            "name": "web"
+          }
+        }
+      ],
       "preBuildCommands": [],
       "dev": {
         "enabled": true,
@@ -582,7 +591,12 @@
     {
       "name": "db",
       "size": "1Gi",
-      "expose": []
+      "expose": [
+        {
+          "network": "Private",
+          "subdomain": "pg"
+        }
+      ]
     }
   ],
   "mongodb": []
@@ -608,11 +622,16 @@
 		"managerAddr":          "",
 		"appId":                "",
 		"sshPrivateKey":        "",
-		"port_service_qwe_ssh": 12,
-		"port_service_qwe_0":   13,
+		"port_service_qwe_ssh": 1,
+		"port_service_qwe_0":   2,
+		"port_postgresql_db_0": 3,
 	}, nil, keyGen)
 	if err != nil {
 		t.Fatal(err)
 	}
-	t.Log(string(r.Raw))
+	access, err := json.Marshal(r.Access)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(string(access))
 }
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index e0aeab2..ffa710c 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -170,8 +170,9 @@
 }
 
 type dodoAppInstallResp struct {
-	Id        string `json:"id"`
-	DeployKey string `json:"deployKey"`
+	Id        string             `json:"id"`
+	DeployKey string             `json:"deployKey"`
+	Access    []installer.Access `json:"access"`
 }
 
 type dodoAppRendered struct {
@@ -224,6 +225,7 @@
 		if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
 			Id:        instanceId,
 			DeployKey: cfg.Input.Key.Public,
+			Access:    rr.Access,
 		}); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 		}
@@ -285,6 +287,17 @@
 	s.tasks[instanceId] = &taskForward{t, fmt.Sprintf("/instance/%s", instanceId), 0}
 	t.OnDone(s.cleanTask(instanceId, 0))
 	go t.Start()
+	var rend dodoAppRendered
+	if err := json.NewDecoder(bytes.NewReader(rr.RenderedRaw)).Decode(&rend); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+	if err := json.NewEncoder(w).Encode(dodoAppInstallResp{
+		Id:        instanceId,
+		DeployKey: rend.Input.Key.Public,
+		Access:    rr.Access,
+	}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
 }
 
 func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {