DodoApp: Abstract away service definition

TODO(gio): There seems to be a performance degradation evaluating cue files.

Change-Id: Ib157dfaa1c108f06f3026032e8fad79c06f42d3a
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 1a60440..d61bfb3 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -2,6 +2,7 @@
 	"crypto/sha256"
 	"encoding/base64"
 	"encoding/json"
+	"strconv"
 	"list"
 	"net"
 	"strings"
@@ -11,6 +12,16 @@
 	cluster?: #Cluster @name(Cluster)
 }
 
+_uuidData: json.Marshal([
+	input,
+	release,
+	global,
+	localCharts,
+					   ])
+// TODO(gio): revisit this logic
+_uuidSorted: strings.Join(list.Sort([for x in strings.Runes(_uuidData) {strconv.QuoteRune(x)}], list.Ascending), "")
+uuid: base64.Encode(null, sha256.Sum256(_uuidSorted))
+
 name:        string | *""
 description: string | *""
 readme:      string | *""
@@ -78,9 +89,9 @@
 	size:       string
 	accessMode: string
 
-	_cluster: cluster
-	_name: name
-	_size: size
+	_cluster:    cluster
+	_name:       name
+	_size:       size
 	_accessMode: accessMode
 
 	charts: {
@@ -98,15 +109,15 @@
 			if _cluster != _|_ {
 				cluster: _cluster
 			}
-			info:  "Creating disk for \(name)"
+			info: "Creating disk for \(name)"
 			annotations: {
 				"dodo.cloud/resource-type":        "volume"
 				"dodo.cloud/resource.volume.name": name
 				"dodo.cloud/resource.volume.size": size
 			}
 			values: {
-				name: _name
-				size: _size
+				name:       _name
+				size:       _size
 				accessMode: _accessMode
 			}
 		}
@@ -115,14 +126,17 @@
 
 #WithOut: {
 	cluster?: #Cluster
-	_cluster: cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	volume: {...}
 	volume: {
 		for k, v in volume {
 			"\(k)": #Volume & v & {
 				name: k
-				if _cluster != _|_ {
-					cluster: _cluster
+				if _cc != null {
+					cluster: _cc
 				}
 			}
 		}
@@ -530,13 +544,16 @@
 
 #WithOut: {
 	cluster?: #Cluster
-	_cluster: cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	mongodb: {...}
 	mongodb: {
 		for k, v in mongodb {
 			"\(k)": #MongoDB & v & {
-				if _cluster != _|_ {
-					cluster: _cluster
+				if _cc != null {
+					cluster: _cc
 				}
 			}
 		}
@@ -708,17 +725,7 @@
 	targetNamespace?: string
 	values: {...} | *{}
 
-	id: base64.Encode(null, sha256.Sum256(json.Marshal([
-		name,
-		values,
-		dependsOn,
-		if cluster != _|_ {
-			cluster
-		},
-		if targetNamespace != _|_ {
-			targetNamespace
-		},
-	])))
+	id: uuid
 	...
 }
 
@@ -831,17 +838,20 @@
 	if input.cluster != _|_ {
 		cluster: #Cluster | *input.cluster
 	}
-	_cluster: cluster
+	_cc: #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	images: {...}
 	charts: {...}
 	helm: {...}
 	openPort: [...#PortForward] | *[]
 	openPortMap: {
 		"_self": list.FlattenN([for i, e in openPort {
-			if cluster == _|_ {
+			if _cc == null {
 				e
 			}
-			if cluster != _|_ {
+			if _cc != null {
 				if strings.ToLower(e.network.name) == "private" {
 					[{
 						name: "\(e.name)_cluster"
@@ -849,16 +859,16 @@
 							name:               "cluster_\(cluster.name)"
 							ingressClass:       "default"
 							domain:             ""
-							allocatePortAddr:   "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/allocate"
-							reservePortAddr:    "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/reserve"
-							deallocatePortAddr: "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/remmove"
+							allocatePortAddr:   "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/allocate"
+							reservePortAddr:    "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/reserve"
+							deallocatePortAddr: "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/remmove"
 						}
 						port:     input[name]
 						protocol: e.protocol
 						service:  e.service
 					}, {
 						name:        e.name
-						clusterName: _cluster.name
+						clusterName: _cc.name
 						network:     e.network
 						port:        input[e.name]
 						protocol:    e.protocol
@@ -872,12 +882,12 @@
 					[{
 						name: "\(e.name)_cluster"
 						network: #Network & {
-							name:               "cluster_\(cluster.name)"
+							name:               "cluster_\(_cc.name)"
 							ingressClass:       "default"
 							domain:             ""
-							allocatePortAddr:   "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/allocate"
-							reservePortAddr:    "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/reserve"
-							deallocatePortAddr: "http://port-allocator.\(global.id)-cluster-\(cluster.name)-network.svc.cluster.local/api/remmove"
+							allocatePortAddr:   "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/allocate"
+							reservePortAddr:    "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/reserve"
+							deallocatePortAddr: "http://port-allocator.\(global.id)-cluster-\(_cc.name)-network.svc.cluster.local/api/remmove"
 						}
 						port:     input[name]
 						protocol: "TCP"
@@ -885,7 +895,7 @@
 					}, {
 						name:        "\(e.name)_private"
 						network:     networks.private // TODO(gio): take corresponding private network
-						clusterName: _cluster.name
+						clusterName: _cc.name
 						port:        input[name]
 						protocol:    "TCP"
 						service: {
@@ -936,8 +946,8 @@
 	helmR: {
 		for k, v in helm {
 			"\(k)": v & {
-				if v.cluster == _|_ && _cluster != _|_ {
-					cluster: _cluster
+				if v.cluster == _|_ && _cc != null {
+					cluster: _cc
 				}
 			}
 		}
@@ -980,8 +990,6 @@
 }
 
 #WithOut: {
-	cluster?: #Cluster
-	_cluster: cluster
 	charts: {
 		secret: {
 			kind:    "GitRepository"
@@ -995,13 +1003,16 @@
 
 #WithOut: {
 	cluster?: #Cluster
-	_cluster: cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	postgresql: {...}
 	postgresql: {
 		for k, v in postgresql {
 			"\(k)": #PostgreSQL & v & {
-				if _cluster != _|_ {
-					cluster: _cluster
+				if _cc != null {
+					cluster: _cc
 				}
 			}
 		}
@@ -1040,14 +1051,17 @@
 
 #WithOut: {
 	cluster?: #Cluster
-	_cluster: cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	vm: {...}
 	vm: {
 		for k, v in vm {
 			"\(k)": #VirtualMachine & v & {
 				name: k
-				if _cluster != _|_ {
-					cluster: _cluster
+				if _cc != null {
+					cluster: _cc
 				}
 			}
 		}
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index cd98be0..e9e5332 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -211,15 +211,18 @@
 
 #WithOut: {
 	cluster?: #Cluster
-	_cluster: cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
 	ingress: {...}
 	ingress: {
 		for k, v in ingress {
 			"\(k)": #Ingress & v & {
 				name: k
 				g:    global
-				if _cluster != _|_ {
-					cluster: _cluster
+				if _cc != null {
+					cluster: _cc
 				}
 			}
 		}
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index 9f41805..bc465b5 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -9,7 +9,12 @@
 namespace: "dodo-app"
 
 cluster?: string
-_cluster: cluster
+_cluster: string | null | *null
+if cluster != _|_ {
+	if cluster != "default" {
+		_cluster: cluster
+	}
+}
 
 input: {
 	// VM uses this endpoint to load env variables from dodo-app server.
@@ -21,7 +26,7 @@
 	key:         #SSHKey
 	// TODO(gio): this should not be necessary as app.dev.username is autogenerated
 	username?: string
-	if _cluster != _|_ {
+	if _cluster != null {
 		cluster: clusterMap[strings.ToLower(_cluster)]
 	}
 
@@ -76,6 +81,7 @@
 _volume:     volume
 _postgresql: postgresql
 _mongodb:    mongodb
+_service:    service
 
 envVars: [
 	for v in _volume {
@@ -452,233 +458,300 @@
 
 service: [...#App]
 
-outs: {
-	for svc in service {
-		_namedPorts: {
-			for p in svc.ports {
-				"\(p.name)": p.value
-			}
+#ServiceDevDisabled: #WithOut & {
+	cluster?: #Cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
+	svc: #App & {
+		dev: {
+			enabled: false
 		}
-		"service_\(svc.name)": #WithOut & {
-			envProfile: strings.Join(list.Concat([
-				svc.vm.env,
-				[for e in svc.lastCmdEnv {"export \(e)"}],
-			]), "\n")
-			if !svc.dev.enabled {
-				{
-					// TODO(gio): support vm open ports
-					openPort: list.Concat([
-						[for i, e in svc.expose if e.port.name != _|_ {
-							for p in svc.ports if e.port.name == p.name {
-								name:     "port_service_\(svc.name)_\(i)"
-								network:  networks[strings.ToLower(e.network)]
-								port:     input[name]
-								protocol: p.protocol
-								service: {
-									name: "app-app"
-									port: p.value
-								}
-							}
-						}],
-						[for i, e in svc.expose if e.port.value != _|_ {
-							for p in svc.ports if e.port.value == p.value {
-								name:     "port_service_\(svc.name)_\(i)"
-								network:  networks[strings.ToLower(e.network)]
-								port:     input[name]
-								protocol: p.protocol
-								service: {
-									name: "app-app"
-									port: p.value
-								}
-							}
-						}],
-					])
-					ingress: {
-						if svc.ingress != _|_ {
-							{for i, ingress in svc.ingress {
-								"\(svc.name)-\(i)": {
-									label:     svc.name
-									network:   networks[strings.ToLower(ingress.network)]
-									subdomain: ingress.subdomain
-									auth:      ingress.auth
-									if input.cluster != _|_ {
-										cluster: input.cluster
-									}
-									service: {
-										name: "\(svc.name)-app"
-										if ingress.port.value != _|_ {
-											port: ingress.port.value
-										}
-										if ingress.port.value == _|_ {
-											port: _namedPorts[ingress.port.name]
-										}
-									}
-								}
-							}}
-						}
-					}
-					images: {
-						app: {
-							repository: "giolekva"
-							name:       "app-runner"
-							tag:        strings.Replace(svc.type, ":", "-", -1)
-							pullPolicy: "Always"
-						}
-						"tailscale-proxy": {
-							repository: "tailscale"
-							name:       "tailscale"
-							tag:        "v1.82.0"
-							pullPolicy: "IfNotPresent"
-						}
-					}
-					charts: {
-						"access-secrets": {
-							kind:    "GitRepository"
-							address: "https://code.v1.dodo.cloud/helm-charts"
-							branch:  "main"
-							path:    "charts/access-secrets"
-						}
-						app: {
-							kind:    "GitRepository"
-							address: "https://code.v1.dodo.cloud/helm-charts"
-							branch:  "main"
-							path:    "charts/app-runner"
-						}
-					}
-					helm: {
-						if input.cluster != _|_ {
-							{
-								"access-secrets": {
-									chart: charts["access-secrets"]
-									values: {
-										serviceAccountName: "default"
-									}
-								}
-							}
-						}
-						"\(svc.name)": {
-							chart: charts.app
-							annotations: {
-								"dodo.cloud/resource-type":         "service"
-								"dodo.cloud/resource.service.name": svc.name
-							}
-							values: {
-								image: {
-									repository: images.app.fullName
-									tag:        images.app.tag
-									pullPolicy: images.app.pullPolicy
-								}
-								// TODO(gio): install gvisor runtime during new remote cluster init
-								if input.cluster == _|_ {
-									runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
-								}
-								name:    svc.name
-								apiPort: svc.apiPort
-								appPorts: [for p in svc.ports {
-									name:          p.name
-									containerPort: p.value
-									protocol:      p.protocol
-								}]
-								appDir:        svc.rootDir
-								appId:         input.appId
-								repoAddr:      svc.source.repository
-								branch:        svc.source.branch
-								rootDir:       svc.source.rootDir
-								sshPrivateKey: base64.Encode(null, input.key.private)
-								runCfg:        base64.Encode(null, json.Marshal(svc.runConfiguration))
-								managerAddr:   input.managerAddr
-								volumes: [
-									for v in svc.volume {
-										name:      v.name
-										mountPath: "/dodo/volume/\(v)"
-									},
-								]
-							}
-						}
-					}
-				}
-			}
-		}
+	}
 
-		if svc.dev.enabled {
-			// TODO(gio): reuse envProfile from above
-			_envProfile: strings.Join(list.Concat([
-				svc.vm.env,
-				[for e in svc.lastCmdEnv {"export \(e)"}],
-			]), "\n")
-			{
-				"\(svc.name)": #WithOut & {
-					ingress: {
-						if svc.ingress != _|_ {
-							{for i, ingress in svc.ingress {
-								"\(svc.name)-\(i)": {
-									label:     svc.name
-									network:   networks[strings.ToLower(ingress.network)]
-									subdomain: ingress.subdomain
-									auth:      ingress.auth
-									service: {
-										name: svc.name
-										if ingress.port.value != _|_ {
-											port: ingress.port.value
-										}
-										if ingress.port.name != _|_ {
-											port: _namedPorts[ingress.port.name]
-										}
-									}
-								}
-								// TODO(gio): code should work even without svc ingress
-								"\(svc.name)-\(i)-code": {
-									label:     "VS Code"
-									home:      "/?folder=/home/\(svc.dev.username)/code"
-									network:   networks[strings.ToLower(ingress.network)]
-									subdomain: "code-\(ingress.subdomain)"
-									auth: enabled: false
-									service: {
-										name: svc.name
-										port: _codeServerPort
-									}
-								}
-							}}
-						}
+	_namedPorts: {
+		for p in svc.ports {
+			"\(p.name)": p.value
+		}
+	}
+
+	openPort: list.Concat([
+		[for i, e in svc.expose if e.port.name != _|_ {
+			for p in svc.ports if e.port.name == p.name {
+				name:     "port_service_\(svc.name)_\(i)"
+				network:  networks[strings.ToLower(e.network)]
+				port:     input[name]
+				protocol: p.protocol
+				service: {
+					name: "app-app"
+					port: p.value
+				}
+			}
+		}],
+		[for i, e in svc.expose if e.port.value != _|_ {
+			for p in svc.ports if e.port.value == p.value {
+				name:     "port_service_\(svc.name)_\(i)"
+				network:  networks[strings.ToLower(e.network)]
+				port:     input[name]
+				protocol: p.protocol
+				service: {
+					name: "app-app"
+					port: p.value
+				}
+			}
+		}],
+	])
+
+	images: {
+		app: {
+			repository: "giolekva"
+			name:       "app-runner"
+			tag:        strings.Replace(svc.type, ":", "-", -1)
+			pullPolicy: "Always"
+		}
+		"tailscale-proxy": {
+			repository: "tailscale"
+			name:       "tailscale"
+			tag:        "v1.82.0"
+			pullPolicy: "IfNotPresent"
+		}
+	}
+
+	charts: {
+		"access-secrets": {
+			kind:    "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch:  "main"
+			path:    "charts/access-secrets"
+		}
+		app: {
+			kind:    "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch:  "main"
+			path:    "charts/app-runner"
+		}
+	}
+
+	ingress: {
+		if svc.ingress != _|_ {
+			{for i, ingress in svc.ingress {
+				"\(svc.name)-\(i)": {
+					label:     svc.name
+					network:   networks[strings.ToLower(ingress.network)]
+					subdomain: ingress.subdomain
+					auth:      ingress.auth
+					if _cc != null {
+						cluster: _cc
 					}
-					vm: {
-						"\(svc.name)": {
-							username: svc.dev.username
-							domain:   global.domain
-							vpn: {
-								enabled:     true
-								loginServer: "https://headscale.\(global.domain)"
-								authKey:     input.vpnAuthKey
-							}
-							codeServerEnabled: true
-							cpuCores:          2
-							memory:            "3Gi"
-							ports:             svc.ports
-							configFiles: {
-								"env.sh": _envProfile
-							}
-							cloudInit: {
-								writeFiles: [{
-									path:        "/home/\(username)/.bash_profile"
-									content:     "source /home/\(username)/.dodo/env.sh"
-									owner:       "\(username):\(username)"
-									permissions: "0700"
-								}]
-								runCmd: list.Concat([[
-									["sh", "-c", "chown \(username):\(username) /home/\(username)/.cache"],
-									["sh", "-c", "GIT_SSH_COMMAND='ssh -i /home/\(username)/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' git clone --branch \(svc.source.branch) \(svc.source.repository) /home/\(username)/code"],
-									["sh", "-c", "chown -R \(username):\(username) /home/\(username)/code"],
-									["sh", "-c", "chown -R \(username):\(username) /home/\(username)"],
-								], svc.vm.cloudInit.runCmd])
-							}
+					service: {
+						name: "\(svc.name)-app"
+						if ingress.port.value != _|_ {
+							port: ingress.port.value
+						}
+						if ingress.port.value == _|_ {
+							port: _namedPorts[ingress.port.name]
 						}
 					}
 				}
+			}}
+		}
+	}
+
+	helm: {
+		if _cc != null {
+			{
+				"access-secrets": {
+					chart: charts["access-secrets"]
+					values: {
+						serviceAccountName: "default"
+					}
+				}
+			}
+		}
+		"\(svc.name)": {
+			chart: charts.app
+			annotations: {
+				"dodo.cloud/resource-type":         "service"
+				"dodo.cloud/resource.service.name": svc.name
+			}
+			values: {
+				image: {
+					repository: images.app.fullName
+					tag:        images.app.tag
+					pullPolicy: images.app.pullPolicy
+				}
+				// TODO(gio): install gvisor runtime during new remote cluster init
+				if cluster == _|_ {
+					runtimeClassName: "untrusted-external" // TODO(gio): make this part of the infra config
+				}
+				name:    svc.name
+				apiPort: svc.apiPort
+				appPorts: [for p in svc.ports {
+					name:          p.name
+					containerPort: p.value
+					protocol:      p.protocol
+				}]
+				appDir:        svc.rootDir
+				appId:         input.appId
+				repoAddr:      svc.source.repository
+				branch:        svc.source.branch
+				rootDir:       svc.source.rootDir
+				sshPrivateKey: base64.Encode(null, input.key.private)
+				runCfg:        base64.Encode(null, json.Marshal(svc.runConfiguration))
+				managerAddr:   input.managerAddr
+				volumes: [
+					for v in svc.volume {
+						name:      v.name
+						mountPath: "/dodo/volume/\(v)"
+					},
+				]
 			}
 		}
 	}
 }
 
+// TODO(gio): Support remote clusters.
+#ServiceDevEnabled: #WithOut & {
+	cluster?: #Cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
+	svc: #App & {
+		dev: {
+			enabled: true
+		}
+	}
+
+	_namedPorts: {
+		for p in svc.ports {
+			"\(p.name)": p.value
+		}
+	}
+
+	// TODO(gio): reuse envProfile from above
+	_envProfile: strings.Join(list.Concat([
+		svc.vm.env,
+		[for e in svc.lastCmdEnv {"export \(e)"}],
+	]), "\n")
+	ingress: {
+		if svc.ingress != _|_ {
+			{for i, ingress in svc.ingress {
+				"\(svc.name)-\(i)": {
+					label:     svc.name
+					network:   networks[strings.ToLower(ingress.network)]
+					subdomain: ingress.subdomain
+					auth:      ingress.auth
+					service: {
+						name: svc.name
+						if ingress.port.value != _|_ {
+							port: ingress.port.value
+						}
+						if ingress.port.name != _|_ {
+							port: _namedPorts[ingress.port.name]
+						}
+					}
+				}
+				// TODO(gio): code should work even without svc ingress
+				"\(svc.name)-\(i)-code": {
+					label:     "VS Code"
+					home:      "/?folder=/home/\(svc.dev.username)/code"
+					network:   networks[strings.ToLower(ingress.network)]
+					subdomain: "code-\(ingress.subdomain)"
+					auth: enabled: false
+					service: {
+						name: svc.name
+						port: _codeServerPort
+					}
+				}
+			}}
+		}
+	}
+	vm: {
+		"\(svc.name)": {
+			username: svc.dev.username
+			domain:   global.domain
+			vpn: {
+				enabled:     true
+				loginServer: "https://headscale.\(global.domain)"
+				authKey:     input.vpnAuthKey
+			}
+			codeServerEnabled: true
+			cpuCores:          2
+			memory:            "3Gi"
+			ports:             svc.ports
+			configFiles: {
+				"env.sh": _envProfile
+			}
+			cloudInit: {
+				writeFiles: [{
+					path:        "/home/\(username)/.bash_profile"
+					content:     "source /home/\(username)/.dodo/env.sh"
+					owner:       "\(username):\(username)"
+					permissions: "0700"
+				}]
+				runCmd: list.Concat([[
+					["sh", "-c", "chown \(username):\(username) /home/\(username)/.cache"],
+					["sh", "-c", "GIT_SSH_COMMAND='ssh -i /home/\(username)/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' git clone --branch \(svc.source.branch) \(svc.source.repository) /home/\(username)/code"],
+					["sh", "-c", "chown -R \(username):\(username) /home/\(username)/code"],
+					["sh", "-c", "chown -R \(username):\(username) /home/\(username)"],
+				], svc.vm.cloudInit.runCmd])
+			}
+		}
+	}
+}
+
+#Service: #ServiceDevEnabled | #ServiceDevDisabled
+
+#WithOut: {
+	cluster?: #Cluster
+	_cc:      #Cluster | null | *null
+	if cluster != _|_ {
+		_cc: cluster
+	}
+	services: {...}
+	services: {
+		for k, v in services {
+			"\(k)": #Service & v & {
+				if _cc != null {
+					cluster: _cc
+				}
+			}
+		}
+		...
+	}
+	openPortMap: {
+		for k, v in services {
+			"service-\(k)": v.openPortMap
+		}
+		...
+	}
+	images: {
+		for k, v in services {
+			for x, y in v.images {
+				"\(x)": y
+			}
+		}
+	}
+	charts: {
+		for k, v in services {
+			for x, y in v.charts {
+				"\(x)": y
+			}
+		}
+	}
+	helmR: {
+		for k, v in services {
+			for x, y in v.helmR {
+				"\(x)": y
+			}
+		}
+		...
+	}
+	...
+}
+
 out: {
 	if input.cluster != _|_ {
 		cluster: input.cluster
@@ -698,6 +771,14 @@
 			"\(v.name)": v
 		}
 	}
+	services: {
+		for v in _service {
+			"\(v.name)": #Service & {
+				name: v.name
+				svc: v
+			}
+		}
+	}
 }
 
 _appDir: "/dodo-app"
diff --git a/core/installer/samples/blog.rest b/core/installer/samples/blog.rest
index 2a0b95c..f0116b7 100644
--- a/core/installer/samples/blog.rest
+++ b/core/installer/samples/blog.rest
@@ -1,4 +1,4 @@
-PUT http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/dodo-app-snd
+POST http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app
 Content-Type: application/json
 
 {
@@ -20,7 +20,7 @@
           "groups": "pr"
         },
         "network": "Private",
-        "subdomain": "blog"
+        "subdomain": "test-blog"
       }],
       "type": "nextjs:deno-2.0.0",
       "preBuildCommands": [
@@ -43,4 +43,4 @@
         }
       ]
   }
-}
\ No newline at end of file
+}
diff --git a/core/installer/status/instance.go b/core/installer/status/instance.go
index c88e5e5..9ad2ff0 100644
--- a/core/installer/status/instance.go
+++ b/core/installer/status/instance.go
@@ -93,6 +93,13 @@
 			s = mergeStatus(status, s)
 		}
 	}
+	for _, i := range out.Services {
+		if s, err := m.monitor(namespace, DodoResource{"service", i.Name}, i, ret); err != nil {
+			return StatusNoStatus, err
+		} else {
+			s = mergeStatus(status, s)
+		}
+	}
 	ret[resource] = status
 	return status, nil
 }
@@ -116,6 +123,7 @@
 	PostgreSQL map[string]ResourceOut     `json:"postgresql"`
 	MongoDB    map[string]ResourceOut     `json:"mongodb"`
 	Ingress    map[string]resourceIngress `json:"ingress"`
+	Services   map[string]ResourceOut     `json:"services"`
 	Helm       map[string]resourceHelm    `json:"helmR"`
 }