AppManager: Support exposing cross-cluster ports

Change-Id: I4bdb3573209935f6777656ec2f3481e79d84a9c9
diff --git a/core/dns-api/init.go b/core/dns-api/init.go
index 0544611..5e494ae 100644
--- a/core/dns-api/init.go
+++ b/core/dns-api/init.go
@@ -40,6 +40,8 @@
 {{- range $i, $ns := .nameserverIP }}
 ns{{ add1 $i }}.{{ $zone }}. 10800 IN A {{ $ns }}
 {{- end }}
+ns.p.{{ $zone }}.	10800	IN	A	100.100.100.100
+devices.p.{{ $zone }}.	10800	IN	NS	ns.p.{{ $zone }}
 {{- range .publicIP }}
 {{ $zone }}. 10800 IN A {{ . }}
 *.{{ $zone }}. 10800 IN A {{ . }}
diff --git a/core/installer/app.go b/core/installer/app.go
index 948e17e..e57fee8 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -95,6 +95,7 @@
 }
 
 type PortForward struct {
+	Cluster     string `json:"cluster,omitempty"`
 	Allocator   string `json:"allocator"`
 	ReserveAddr string `json:"reservator"`
 	RemoveAddr  string `json:"deallocator"`
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 7698553..7330b1e 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -109,9 +109,9 @@
 	imageRegistry: string | *"docker.io"
 }
 
-// TODO(gio): Support inter-cluster porf forwarding
 #PortForward: {
 	network: #Network
+	cluster?: string | null
 	port: int
 	service: close({
 		name: string
@@ -507,15 +507,44 @@
 	_volumeClaimName: "\(name)-postgresql"
 	_name: name
 
-	openPort: [for i, e in expose {
-		network: networks[strings.ToLower(e.network)]
-		port: input["port_postgresql_\(_name)_\(i)"]
-		protocol: "TCP"
-		service: {
-			name: "postgres-\(_name)"
-			port: 5432
+	openPort: list.FlattenN([for i, e in expose {
+		if cluster == _|_ {
+			network: networks[strings.ToLower(e.network)]
+			port: input["port_postgresql_\(_name)_\(i)"]
+			protocol: "TCP"
+			service: {
+				name: "postgres-\(_name)"
+				port: 5432
+			}
 		}
-	}]
+		if cluster != _|_ {
+			[{
+				network: #Network & {
+					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"
+				}
+				port: input["port_postgresql_\(_name)_\(i)_cluster"]
+				protocol: "TCP"
+				service: {
+					name: "postgres-\(_name)"
+					port: 5432
+				}
+			}, {
+				cluster: _cluster.name
+				network: networks[strings.ToLower(e.network)]
+				port: input["port_postgresql_\(_name)_\(i)"]
+				protocol: "TCP"
+				service: {
+					name: "cluster-\(cluster).private-network-proxy.devices.\(global.privateDomain)"
+					port: input["port_postgresql_\(_name)_\(i)_cluster"]
+				}
+			}]
+		}
+	}], -1)
 
 	images: {
 		postgres: {
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index 3841f02..6a02df2 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -62,8 +62,8 @@
 	if _cluster != _|_ {
 		clusterProxy: {
 			"\(name)": {
-				from: _domain
 				_sanitizedDomain: strings.Replace(_domain, ".", "-", -1)
+				from: _domain
 				to: "\(_sanitizedDomain).\(_cluster.name).cluster.\(global.privateDomain)"
 			}
 		}
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index bf5a7f8..6f7001a 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -24,6 +24,9 @@
 	for v in _postgresql {
 		for i, e in v.expose {
 			"port_postgresql_\(v.name)_\(i)": int @role(port)
+			if input.cluster != _|_ {
+				"port_postgresql_\(v.name)_\(i)_cluster": int @role(port)
+			}
 		}
 	}
 	for v in _mongodb {
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index c5b849f..3285c19 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -182,10 +182,19 @@
 	Secret string `json:"secret"`
 }
 
-func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
+type reservePortInfo struct {
+	reserveAddr string
+	RemoteProxy bool `json:"remoteProxy"`
+}
+
+func reservePorts(ports map[string]reservePortInfo) (map[string]reservePortResp, error) {
 	ret := map[string]reservePortResp{}
-	for p, reserveAddr := range ports {
-		resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
+	for p, cfg := range ports {
+		var buf bytes.Buffer
+		if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
+			return nil, err
+		}
+		resp, err := http.Post(cfg.reserveAddr, "application/json", &buf)
 		if err != nil {
 			return nil, err
 		}
@@ -205,11 +214,17 @@
 
 func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
 	for _, p := range ports {
+		var target string
+		if p.Cluster == "" {
+			target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
+		} else {
+			target = p.Service.Name
+		}
 		var buf bytes.Buffer
 		req := allocatePortReq{
 			Protocol:      p.Protocol,
 			SourcePort:    p.Port,
-			TargetService: fmt.Sprintf("%s/%s", ns, p.Service.Name),
+			TargetService: target,
 			TargetPort:    p.Service.Port,
 		}
 		allocator := ""
@@ -475,10 +490,13 @@
 	if err != nil {
 		return ReleaseResources{}, err
 	}
-	reservators := map[string]string{}
+	reservators := map[string]reservePortInfo{}
 	allocators := map[string]string{}
 	for _, pf := range rendered.Ports {
-		reservators[portFields[pf.Port]] = pf.ReserveAddr
+		reservators[portFields[pf.Port]] = reservePortInfo{
+			reserveAddr: pf.ReserveAddr,
+			RemoteProxy: pf.Cluster != "",
+		}
 		allocators[portFields[pf.Port]] = pf.Allocator
 	}
 	portReservations, err := reservePorts(reservators)
@@ -530,7 +548,7 @@
 		return ReleaseResources{}, err
 	}
 	for _, p := range rendered.ClusterProxies {
-		if err := m.cnc.AddProxy(p.From, p.To); err != nil {
+		if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
 			return ReleaseResources{}, err
 		}
 	}
@@ -651,7 +669,7 @@
 			}
 		}
 		if !found {
-			if err := m.cnc.RemoveProxy(ocp.From, ocp.To); err != nil {
+			if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
 				return ReleaseResources{}, err
 			}
 		}
@@ -665,7 +683,7 @@
 			}
 		}
 		if !found {
-			if err := m.cnc.AddProxy(ncp.From, ncp.To); err != nil {
+			if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
 				return ReleaseResources{}, err
 			}
 		}
@@ -723,7 +741,7 @@
 		return err
 	}
 	for _, cp := range cfg.Out.ClusterProxy {
-		if err := m.cnc.RemoveProxy(cp.From, cp.To); err != nil {
+		if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
 			return err
 		}
 	}
diff --git a/core/installer/canvas-app.cue b/core/installer/canvas-app.cue
index a8be305..6843277 100644
--- a/core/installer/canvas-app.cue
+++ b/core/installer/canvas-app.cue
@@ -24,14 +24,14 @@
   		    "enabled": false
 		}
 	}],
-	  "mongodb": [{
+	  "postgresql": [{
 		  "name": "pgg",
 		  "size": "2Gi",
 		  "expose": [{
 		    "network": "private",
-			"subdomain": "mongoo"
+			"subdomain": "pggp"
 		  }]
 	  }],
-	  "cluster": "ct"
+	  "cluster": "asdc"
   }
 }
diff --git a/core/installer/cluster.go b/core/installer/cluster.go
index 19220af..ac7d970 100644
--- a/core/installer/cluster.go
+++ b/core/installer/cluster.go
@@ -16,21 +16,26 @@
 
 	"github.com/giolekva/pcloud/core/installer/soft"
 
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
 	"sigs.k8s.io/yaml"
 )
 
 type ClusterNetworkConfigurator interface {
 	AddCluster(name string, ingressIP net.IP) error
 	RemoveCluster(name string, ingressIP net.IP) error
-	AddProxy(src, dst string) error
-	RemoveProxy(src, dst string) error
+	AddProxy(src int, dst string, protocol Protocol) (string, error)
+	AddIngressProxy(src, dst string) error
+	RemoveProxy(src int, dst string, protocol Protocol) error
+	RemoveIngressProxy(src, dst string) error
 }
 
 type NginxProxyConfigurator struct {
 	PrivateSubdomain string
 	DNSAPIAddr       string
 	Repo             soft.RepoIO
-	NginxConfigPath  string
+	ConfigPath       string
+	ServicePath      string
 }
 
 type createARecordReq struct {
@@ -80,10 +85,47 @@
 	return nil
 }
 
-func (c *NginxProxyConfigurator) AddProxy(src, dst string) error {
+func (c *NginxProxyConfigurator) AddProxy(src int, dst string, protocol Protocol) (string, error) {
+	var namespace string
 	_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
+		if err := func() error {
+			r, err := fs.Reader(c.ServicePath)
+			if err != nil {
+				return err
+			}
+			defer r.Close()
+			var buf bytes.Buffer
+			if _, err := io.Copy(&buf, r); err != nil {
+				return err
+			}
+			var svc corev1.Service
+			if err := yaml.Unmarshal(buf.Bytes(), &svc); err != nil {
+				return err
+			}
+			svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
+				Name:       fmt.Sprintf("p%d", src),
+				Protocol:   corev1.Protocol(ProtocolToString(protocol)),
+				Port:       int32(src),
+				TargetPort: intstr.FromInt(src),
+			})
+			w, err := fs.Writer(c.ServicePath)
+			if err != nil {
+				return err
+			}
+			defer w.Close()
+			tmp, err := yaml.Marshal(svc)
+			if err != nil {
+				return err
+			}
+			if _, err := io.Copy(w, bytes.NewReader(tmp)); err != nil {
+				return err
+			}
+			return nil
+		}(); err != nil {
+			return "", err
+		}
 		cfg, err := func() (NginxProxyConfig, error) {
-			r, err := fs.Reader(c.NginxConfigPath)
+			r, err := fs.Reader(c.ConfigPath)
 			if err != nil {
 				return NginxProxyConfig{}, err
 			}
@@ -93,11 +135,19 @@
 		if err != nil {
 			return "", err
 		}
-		if v, ok := cfg.Proxies[src]; ok && v != dst {
-			return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
+		namespace = cfg.Namespace
+		var proxyMap map[int]string
+		switch protocol {
+		case ProtocolTCP:
+			proxyMap = cfg.TCP
+		case ProtocolUDP:
+			proxyMap = cfg.UDP
+		default:
+			return "", fmt.Errorf("invalid protocol: %v", protocol)
 		}
-		cfg.Proxies[src] = dst
-		w, err := fs.Writer(c.NginxConfigPath)
+		// TODO(gio): check for already existing mapping
+		proxyMap[src] = dst
+		w, err := fs.Writer(c.ConfigPath)
 		if err != nil {
 			return "", err
 		}
@@ -108,7 +158,7 @@
 			return "", err
 		}
 		hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
-		nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
+		nginxPath := filepath.Join(filepath.Dir(c.ConfigPath), "ingress-nginx.yaml")
 		nginx, err := func() (map[string]any, error) {
 			r, err := fs.Reader(nginxPath)
 			if err != nil {
@@ -149,15 +199,18 @@
 		if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
 			return "", err
 		}
-		return fmt.Sprintf("add proxy mapping: %s %s", src, dst), nil
+		return fmt.Sprintf("add proxy mapping: %d %s", src, dst), nil
 	})
-	return err
+	if err != nil {
+		return "", err
+	}
+	return namespace, nil
 }
 
-func (c *NginxProxyConfigurator) RemoveProxy(src, dst string) error {
+func (c *NginxProxyConfigurator) AddIngressProxy(src, dst string) error {
 	_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
 		cfg, err := func() (NginxProxyConfig, error) {
-			r, err := fs.Reader(c.NginxConfigPath)
+			r, err := fs.Reader(c.ConfigPath)
 			if err != nil {
 				return NginxProxyConfig{}, err
 			}
@@ -167,11 +220,11 @@
 		if err != nil {
 			return "", err
 		}
-		if v, ok := cfg.Proxies[src]; !ok || v != dst {
-			return "", fmt.Errorf("wrong mapping from source: %s actual: %s expected: %s", src, v, dst)
+		if v, ok := cfg.Ingress[src]; ok && v != dst {
+			return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
 		}
-		delete(cfg.Proxies, src)
-		w, err := fs.Writer(c.NginxConfigPath)
+		cfg.Ingress[src] = dst
+		w, err := fs.Writer(c.ConfigPath)
 		if err != nil {
 			return "", err
 		}
@@ -182,7 +235,198 @@
 			return "", err
 		}
 		hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
-		nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
+		nginxPath := filepath.Join(filepath.Dir(c.ConfigPath), "ingress-nginx.yaml")
+		nginx, err := func() (map[string]any, error) {
+			r, err := fs.Reader(nginxPath)
+			if err != nil {
+				return nil, err
+			}
+			defer r.Close()
+			var buf bytes.Buffer
+			if _, err := io.Copy(&buf, r); err != nil {
+				return nil, err
+			}
+			ret := map[string]any{}
+			if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
+				return nil, err
+			}
+			return ret, nil
+		}()
+		if err != nil {
+			return "", err
+		}
+		cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
+		var annotations map[string]any
+		if a, ok := cv["podAnnotations"]; ok {
+			annotations = a.(map[string]any)
+		} else {
+			annotations = map[string]any{}
+			cv["podAnnotations"] = annotations
+		}
+		annotations["dodo.cloud/hash"] = string(hash)
+		buf, err := yaml.Marshal(nginx)
+		if err != nil {
+			return "", err
+		}
+		w, err = fs.Writer(nginxPath)
+		if err != nil {
+			return "", err
+		}
+		defer w.Close()
+		if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("add ingress proxy mapping: %s %s", src, dst), nil
+	})
+	return err
+}
+
+func (c *NginxProxyConfigurator) RemoveProxy(src int, dst string, protocol Protocol) error {
+	_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
+		if err := func() error {
+			r, err := fs.Reader(c.ServicePath)
+			if err != nil {
+				return err
+			}
+			defer r.Close()
+			var buf bytes.Buffer
+			if _, err := io.Copy(&buf, r); err != nil {
+				return err
+			}
+			var svc corev1.Service
+			if err := yaml.Unmarshal(buf.Bytes(), &svc); err != nil {
+				return err
+			}
+			for i, p := range svc.Spec.Ports {
+				if p.Port == int32(src) {
+					svc.Spec.Ports = append(svc.Spec.Ports[:i], svc.Spec.Ports[i+1:]...)
+					break
+				}
+			}
+			w, err := fs.Writer(c.ServicePath)
+			if err != nil {
+				return err
+			}
+			defer w.Close()
+			tmp, err := yaml.Marshal(svc)
+			if err != nil {
+				return err
+			}
+			if _, err := io.Copy(w, bytes.NewReader(tmp)); err != nil {
+				return err
+			}
+			return nil
+		}(); err != nil {
+			return "", err
+		}
+		cfg, err := func() (NginxProxyConfig, error) {
+			r, err := fs.Reader(c.ConfigPath)
+			if err != nil {
+				return NginxProxyConfig{}, err
+			}
+			defer r.Close()
+			return ParseNginxProxyConfig(r)
+		}()
+		if err != nil {
+			return "", err
+		}
+		var proxyMap map[int]string
+		switch protocol {
+		case ProtocolTCP:
+			proxyMap = cfg.TCP
+		case ProtocolUDP:
+			proxyMap = cfg.UDP
+		default:
+			return "", fmt.Errorf("invalid protocol: %v", protocol)
+		}
+		// TODO(gio): check for already existing mapping
+		delete(proxyMap, src)
+		w, err := fs.Writer(c.ConfigPath)
+		if err != nil {
+			return "", err
+		}
+		defer w.Close()
+		h := sha256.New()
+		o := io.MultiWriter(w, h)
+		if err := cfg.Render(o); err != nil {
+			return "", err
+		}
+		hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
+		nginxPath := filepath.Join(filepath.Dir(c.ConfigPath), "ingress-nginx.yaml")
+		nginx, err := func() (map[string]any, error) {
+			r, err := fs.Reader(nginxPath)
+			if err != nil {
+				return nil, err
+			}
+			defer r.Close()
+			var buf bytes.Buffer
+			if _, err := io.Copy(&buf, r); err != nil {
+				return nil, err
+			}
+			ret := map[string]any{}
+			if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
+				return nil, err
+			}
+			return ret, nil
+		}()
+		if err != nil {
+			return "", err
+		}
+		cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
+		var annotations map[string]any
+		if a, ok := cv["podAnnotations"]; ok {
+			annotations = a.(map[string]any)
+		} else {
+			annotations = map[string]any{}
+			cv["podAnnotations"] = annotations
+		}
+		annotations["dodo.cloud/hash"] = string(hash)
+		buf, err := yaml.Marshal(nginx)
+		if err != nil {
+			return "", err
+		}
+		w, err = fs.Writer(nginxPath)
+		if err != nil {
+			return "", err
+		}
+		defer w.Close()
+		if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("remove proxy mapping: %d %s", src, dst), nil
+	})
+	return err
+}
+
+func (c *NginxProxyConfigurator) RemoveIngressProxy(src, dst string) error {
+	_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
+		cfg, err := func() (NginxProxyConfig, error) {
+			r, err := fs.Reader(c.ConfigPath)
+			if err != nil {
+				return NginxProxyConfig{}, err
+			}
+			defer r.Close()
+			return ParseNginxProxyConfig(r)
+		}()
+		if err != nil {
+			return "", err
+		}
+		if v, ok := cfg.Ingress[src]; !ok || v != dst {
+			return "", fmt.Errorf("wrong mapping from source: %s actual: %s expected: %s", src, v, dst)
+		}
+		delete(cfg.Ingress, src)
+		w, err := fs.Writer(c.ConfigPath)
+		if err != nil {
+			return "", err
+		}
+		defer w.Close()
+		h := sha256.New()
+		o := io.MultiWriter(w, h)
+		if err := cfg.Render(o); err != nil {
+			return "", err
+		}
+		hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
+		nginxPath := filepath.Join(filepath.Dir(c.ConfigPath), "ingress-nginx.yaml")
 		nginx, err := func() (map[string]any, error) {
 			r, err := fs.Reader(nginxPath)
 			if err != nil {
@@ -228,11 +472,40 @@
 	return err
 }
 
+type Protocol int
+
+const (
+	ProtocolTCP Protocol = iota
+	ProtocolUDP
+)
+
+func ProtocolToString(p Protocol) string {
+	if p == ProtocolTCP {
+		return "TCP"
+	} else {
+		return "UDP"
+	}
+}
+
 type NginxProxyConfig struct {
-	Port      int
-	Resolvers []net.IP
-	Proxies   map[string]string
-	PreConf   []string
+	Namespace   string
+	IngressPort int
+	Resolvers   []net.IP
+	Ingress     map[string]string
+	TCP         map[int]string
+	UDP         map[int]string
+	PreConf     []string
+}
+
+func parseProtocol(s string) (Protocol, error) {
+	switch strings.ToLower(s) {
+	case "tcp":
+		return ProtocolTCP, nil
+	case "udp":
+		return ProtocolUDP, nil
+	default:
+		return ProtocolUDP, fmt.Errorf("invalid protocol: %s", s)
+	}
 }
 
 func ParseNginxProxyConfig(r io.Reader) (NginxProxyConfig, error) {
@@ -241,13 +514,19 @@
 		return NginxProxyConfig{}, err
 	}
 	ret := NginxProxyConfig{
-		Port:      -1,
-		Resolvers: nil,
-		Proxies:   make(map[string]string),
+		IngressPort: -1,
+		Resolvers:   nil,
+		Ingress:     make(map[string]string),
+		TCP:         make(map[int]string),
+		UDP:         make(map[int]string),
 	}
 	lines := strings.Split(buf.String(), "\n")
-	insideConf := true
+	insidePreConf := true
+	insideHttp := false
 	insideMap := false
+	insideStream := false
+	streamPort := -1
+	streamPortProtocol := ProtocolTCP
 	for _, l := range lines {
 		items := strings.Fields(strings.TrimSuffix(l, ";"))
 		if len(items) == 0 {
@@ -255,19 +534,49 @@
 		}
 		if strings.Contains(l, "nginx.conf") {
 			ret.PreConf = append(ret.PreConf, l)
-			insideConf = false
-		} else if insideConf {
+			insidePreConf = false
+		} else if insidePreConf {
 			ret.PreConf = append(ret.PreConf, l)
+			items := strings.Fields(l)
+			if items[0] == "namespace:" {
+				ret.Namespace = items[1]
+			}
+		} else if items[0] == "http" {
+			insideHttp = true
+		} else if insideHttp && items[0] == "map" {
+			insideMap = true
+		} else if items[0] == "stream" {
+			insideHttp = false
+			insideMap = false
+			insideStream = true
 		} else if strings.Contains(l, "listen") {
 			if len(items) < 2 {
-				return NginxProxyConfig{}, fmt.Errorf("invalid listen: %s\n", l)
+				return NginxProxyConfig{}, fmt.Errorf("invalid listen: %s", l)
 			}
 			port, err := strconv.Atoi(items[1])
 			if err != nil {
 				return NginxProxyConfig{}, err
 			}
-			ret.Port = port
-		} else if strings.Contains(l, "resolver") {
+			if insideHttp {
+				if len(items) > 2 {
+					return NginxProxyConfig{}, fmt.Errorf("invalid http listen: %s", l)
+				}
+				ret.IngressPort = port
+			} else {
+				if !insideStream {
+					return NginxProxyConfig{}, fmt.Errorf("invalid state, expected to be inside stream section")
+				}
+				streamPort = port
+				if len(items) == 3 {
+					streamPortProtocol, err = parseProtocol(items[2])
+					if err != nil {
+						return NginxProxyConfig{}, err
+					}
+				} else {
+					streamPortProtocol = ProtocolTCP
+				}
+			}
+		} else if insideHttp && strings.Contains(l, "resolver") {
 			if len(items) < 2 {
 				return NginxProxyConfig{}, fmt.Errorf("invalid resolver: %s", l)
 			}
@@ -276,7 +585,7 @@
 				return NginxProxyConfig{}, fmt.Errorf("invalid resolver ip: %s", l)
 			}
 			ret.Resolvers = append(ret.Resolvers, ip)
-		} else if insideMap {
+		} else if insideHttp && insideMap {
 			if items[0] == "}" {
 				insideMap = false
 				continue
@@ -284,9 +593,19 @@
 			if len(items) < 2 {
 				return NginxProxyConfig{}, fmt.Errorf("invalid map: %s", l)
 			}
-			ret.Proxies[items[0]] = items[1]
-		} else if items[0] == "map" {
-			insideMap = true
+			ret.Ingress[items[0]] = items[1]
+		} else if insideStream && strings.Contains(l, "proxy_pass") {
+			if streamPort == -1 {
+				return NginxProxyConfig{}, fmt.Errorf("invalid state, expected server port to be defined")
+			}
+			if len(items) < 2 {
+				return NginxProxyConfig{}, fmt.Errorf("invalid proxy_pass: %s", l)
+			}
+			if streamPortProtocol == ProtocolTCP {
+				ret.TCP[streamPort] = items[1]
+			} else {
+				ret.UDP[streamPort] = items[1]
+			}
 		}
 	}
 	return ret, nil
@@ -310,12 +629,12 @@
     }
     http {
         map $http_host $backend {
-            {{- range $from, $to := .Proxies }}
+            {{- range $from, $to := .Ingress }}
             {{ $from }} {{ $to }};
             {{- end }}
         }
         server {
-            listen {{ .Port }};
+            listen {{ .IngressPort }};
             location / {
                 {{- range .Resolvers }}
                 resolver {{ . }};
@@ -323,4 +642,22 @@
                 proxy_pass http://$backend;
             }
         }
-    }`
+    }
+    {{- if or .TCP .UDP }}
+    stream {
+        {{- range $port, $upstream := .TCP }}
+        server {
+            listen {{ $port }};
+            resolver 100.100.100.100;
+            proxy_pass {{ $upstream }};
+        }
+        {{- end  }}
+        {{- range $port, $upstream := .UDP }}
+        server {
+            listen {{ $port }} udp;
+            resolver 100.100.100.100;
+            proxy_pass {{ $upstream }};
+        }
+        {{- end  }}
+    }
+    {{- end }}`
diff --git a/core/installer/cluster_test.go b/core/installer/cluster_test.go
index e315202..b2ecd99 100644
--- a/core/installer/cluster_test.go
+++ b/core/installer/cluster_test.go
@@ -7,7 +7,14 @@
 )
 
 func TestParseNginxProxyConfig(t *testing.T) {
-	cfg, err := ParseNginxProxyConfig(strings.NewReader(`nginx.conf: |
+	cfg, err := ParseNginxProxyConfig(strings.NewReader(`
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: proxy-backend-config
+  namespace: foo
+data:
+nginx.conf: |
 # user       www www;
 worker_processes  1;
 error_log   /dev/null   crit;
@@ -33,33 +40,66 @@
 		}
 	}
 }
+stream {
+	server {
+		listen 9091;
+		proxy_pass foo:1;
+	}
+	server {
+		listen 9092 udp;
+		proxy_pass bar:2;
+	}
+	server {
+		listen 9093;
+		proxy_pass dev:3;
+	}
+}
 `))
 	if err != nil {
 		t.Fatal(err)
 	}
-	if cfg.Port != 9090 {
-		t.Errorf("invalid port: expected 9090, got %d", cfg.Port)
+	if cfg.Namespace != "foo" {
+		t.Errorf("invalid namespace: expeced foo, got %s", cfg.Namespace)
+	}
+	if cfg.IngressPort != 9090 {
+		t.Errorf("invalid port: expected 9090, got %d", cfg.IngressPort)
 	}
 	if len(cfg.Resolvers) != 2 ||
 		!cfg.Resolvers[0].Equal(net.ParseIP("1.1.1.1")) ||
 		!cfg.Resolvers[1].Equal(net.ParseIP("2.2.2.2")) {
 		t.Errorf("invalid resolvers: expected [1.1.1.1 2.2.2.2], got %s", cfg.Resolvers)
 	}
-	if len(cfg.Proxies) != 2 ||
-		cfg.Proxies["a"] != "A" ||
-		cfg.Proxies["b"] != "B" {
-		t.Errorf("invalid proxies: expected map[a:A, b:B], got %s", cfg.Proxies)
+	if len(cfg.Ingress) != 2 ||
+		cfg.Ingress["a"] != "A" ||
+		cfg.Ingress["b"] != "B" {
+		t.Errorf("invalid ingress proxies: expected map[a:A, b:B], got %s", cfg.Ingress)
+	}
+	if len(cfg.TCP) != 2 ||
+		cfg.TCP[9091] != "foo:1" ||
+		cfg.TCP[9093] != "dev:3" {
+		t.Errorf("invalid TCP proxies: expected map[9091:foo:1, 9093:dev:3], got %v", cfg.TCP)
+	}
+	if len(cfg.UDP) != 1 ||
+		cfg.UDP[9092] != "bar:2" {
+		t.Errorf("invalid UDP proxies: expected map[9092:bar:2], got %v", cfg.UDP)
 	}
 }
 
 func TestRenderNginxProxyConfig(t *testing.T) {
 	cfg := NginxProxyConfig{
-		Port:      8080,
-		Resolvers: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("2.2.2.2")},
-		Proxies: map[string]string{
+		IngressPort: 8080,
+		Resolvers:   []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("2.2.2.2")},
+		Ingress: map[string]string{
 			"a": "A",
 			"b": "B",
 		},
+		TCP: map[int]string{
+			1: "foo:1",
+			3: "dev:3",
+		},
+		UDP: map[int]string{
+			2: "bar:2",
+		},
 		PreConf: []string{"line1", "line2"},
 	}
 	var buf strings.Builder
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index afc7d32..c2547d6 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -3,8 +3,8 @@
 import (
 	"log"
 	"os"
-
-	"golang.org/x/crypto/ssh"
+	"path/filepath"
+	"strings"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/server/appmanager"
@@ -80,20 +80,14 @@
 	if err != nil {
 		return err
 	}
-	signer, err := ssh.ParsePrivateKey(sshKey)
+	items := strings.Split(appManagerFlags.repoAddr, "/")
+	ipPort := items[len(items)-2]
+	repoName := items[len(items)-1]
+	ssClient, err := soft.NewClient(ipPort, sshKey, log.Default())
 	if err != nil {
 		return err
 	}
-	addr, err := soft.ParseRepositoryAddress(appManagerFlags.repoAddr)
-	if err != nil {
-		return err
-	}
-	repo, err := soft.CloneRepository(addr, signer)
-	if err != nil {
-		return err
-	}
-	log.Println("Cloned repository")
-	repoIO, err := soft.NewRepoIO(repo, signer)
+	repoIO, err := ssClient.GetRepo(repoName)
 	if err != nil {
 		return err
 	}
@@ -112,7 +106,8 @@
 		PrivateSubdomain: "p",
 		DNSAPIAddr:       appManagerFlags.dnsAPIAddr,
 		Repo:             repoIO,
-		NginxConfigPath:  appManagerFlags.clusterProxyConfigPath,
+		ConfigPath:       appManagerFlags.clusterProxyConfigPath,
+		ServicePath:      filepath.Join(filepath.Dir(appManagerFlags.clusterProxyConfigPath), "proxy-backend-service.yaml"),
 	}
 	m, err := installer.NewAppManager(repoIO, nsc, jc, hf, vpnAPIClient, cnc, "/apps")
 	if err != nil {
@@ -145,6 +140,7 @@
 	}
 	s, err := appmanager.NewServer(
 		appManagerFlags.port,
+		ssClient,
 		repoIO,
 		m,
 		r,
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index adc8beb..9b5da30 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -245,6 +245,8 @@
 	return s.Start()
 }
 
+// TODO(gio): this should be removed, all of app installtions including dodo app
+// must be done directly by main app manager.
 type proxyConfigurator struct {
 	apiAddr string
 }
@@ -262,7 +264,7 @@
 	To   string `json:"to"`
 }
 
-func (pc *proxyConfigurator) AddProxy(src, dst string) error {
+func (pc *proxyConfigurator) AddIngressProxy(src, dst string) error {
 	var buf bytes.Buffer
 	if err := json.NewEncoder(&buf).Encode(proxyPair{src, dst}); err != nil {
 		return err
@@ -279,7 +281,17 @@
 	return nil
 }
 
-func (pc *proxyConfigurator) RemoveProxy(src, dst string) error {
+func (pc *proxyConfigurator) AddProxy(src int, dst string, protocol installer.Protocol) (string, error) {
+	// TODO(gio): implement
+	return "", fmt.Errorf("NOT IMPLEMENTED")
+}
+
+func (pc *proxyConfigurator) RemoveProxy(src int, dst string, protocol installer.Protocol) error {
+	// TODO(gio): implement
+	return fmt.Errorf("NOT IMPLEMENTED")
+}
+
+func (pc *proxyConfigurator) RemoveIngressProxy(src, dst string) error {
 	var buf bytes.Buffer
 	if err := json.NewEncoder(&buf).Encode(proxyPair{src, dst}); err != nil {
 		return err
diff --git a/core/installer/dodo_app_test.go b/core/installer/dodo_app_test.go
index 5b0f3b1..eaad590 100644
--- a/core/installer/dodo_app_test.go
+++ b/core/installer/dodo_app_test.go
@@ -1,6 +1,8 @@
 package installer
 
 import (
+	"bytes"
+	"encoding/json"
 	"testing"
 
 	"cuelang.org/go/cue/errors"
@@ -210,3 +212,61 @@
 	}
 	t.Log(string(r.Raw))
 }
+
+const exposeRemoteCluster = `
+{
+    "cluster": "remote",
+    "postgresql": [{
+		"name": "db",
+		"size": "1Gi",
+		"expose": [{
+			"network": "Private",
+			"subdomain": "pg"
+		}]
+	}],
+}
+`
+
+func TestExposeRemoteCluster(t *testing.T) {
+	var buf bytes.Buffer
+	if _, err := buf.WriteString(exposeRemoteCluster); err != nil {
+		t.Fatal(err)
+	}
+	clusters := []Cluster{{
+		Name:             "remote",
+		Kubeconfig:       "<KUBECONFIG>",
+		IngressClassName: "<INGRESS_CLASS_NAME>",
+	}}
+	if err := json.NewEncoder(&buf).Encode(struct {
+		Clusters []Cluster `json:"clusters"`
+	}{
+		clusters,
+	}); err != nil {
+		t.Fatal(err)
+	}
+	app, err := NewDodoApp(buf.Bytes())
+	if err != nil {
+		for _, e := range errors.Errors(err) {
+			t.Log(e)
+		}
+		t.Fatal(err)
+	}
+	release := Release{
+		Namespace:     "foo",
+		AppInstanceId: "foo-bar",
+		RepoAddr:      "ssh://192.168.100.210:22/config",
+		AppDir:        "/foo/bar",
+	}
+	keyGen := testKeyGen{}
+	r, err := app.Render(release, env, networks, clusters, map[string]any{
+		"managerAddr":                  "",
+		"appId":                        "",
+		"sshPrivateKey":                "",
+		"port_postgresql_db_0":         1,
+		"port_postgresql_db_0_cluster": 2,
+	}, nil, keyGen)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(string(r.Raw))
+}
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index b3a0883..3360ff4 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -41,6 +41,7 @@
 type Server struct {
 	l            sync.Locker
 	port         int
+	ssClient     soft.Client
 	repo         soft.RepoIO
 	m            *installer.AppManager
 	r            installer.AppRepository
@@ -98,6 +99,7 @@
 
 func NewServer(
 	port int,
+	ssClient soft.Client,
 	repo soft.RepoIO,
 	m *installer.AppManager,
 	r installer.AppRepository,
@@ -114,6 +116,7 @@
 	return &Server{
 		l:            &sync.Mutex{},
 		port:         port,
+		ssClient:     ssClient,
 		repo:         repo,
 		m:            m,
 		r:            r,
@@ -187,7 +190,7 @@
 		return
 	}
 	appDir := filepath.Join("/dodo-app", req.Id)
-	namespace := "dodo-app-test" // TODO(gio)
+	namespace := "dodo-app-testttt" // TODO(gio)
 	if _, err := s.m.Install(app, req.Id, appDir, namespace, map[string]any{
 		"managerAddr":   "", // TODO(gio)
 		"appId":         req.Id,
@@ -238,7 +241,7 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	if err := s.cnc.AddProxy(req.From, req.To); err != nil {
+	if err := s.cnc.AddIngressProxy(req.From, req.To); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -250,7 +253,7 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	if err := s.cnc.RemoveProxy(req.From, req.To); err != nil {
+	if err := s.cnc.RemoveIngressProxy(req.From, req.To); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -891,6 +894,17 @@
 			if err != nil {
 				return installer.ReleaseResources{}, err
 			}
+			keys, err := installer.NewSSHKeyPair("port-allocator")
+			if err != nil {
+				return installer.ReleaseResources{}, err
+			}
+			user := fmt.Sprintf("%s-cluster-%s-port-allocator", env.Id, name)
+			if err := s.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
+				return installer.ReleaseResources{}, err
+			}
+			if err := s.ssClient.AddReadWriteCollaborator("config", user); err != nil {
+				return installer.ReleaseResources{}, err
+			}
 			instanceId := fmt.Sprintf("%s-%s", app.Slug(), name)
 			appDir := fmt.Sprintf("/clusters/%s/ingress", name)
 			namespace := fmt.Sprintf("%scluster-%s-network", env.NamespacePrefix, name)
@@ -903,6 +917,7 @@
 				// TODO(gio): remove hardcoded user
 				"vpnUser":          vpnUser,
 				"vpnProxyHostname": hostname,
+				"sshPrivateKey":    string(keys.RawPrivateKey()),
 			})
 			if err != nil {
 				return installer.ReleaseResources{}, err
diff --git a/core/installer/values-tmpl/cluster-network.cue b/core/installer/values-tmpl/cluster-network.cue
index 393ac41..85551e6 100644
--- a/core/installer/values-tmpl/cluster-network.cue
+++ b/core/installer/values-tmpl/cluster-network.cue
@@ -1,5 +1,5 @@
 import (
-	// "encoding/base64"
+	"encoding/base64"
 )
 
 input: {
@@ -7,7 +7,7 @@
 	vpnUser: string
 	vpnProxyHostname: string
 	vpnAuthKey: string @role(VPNAuthKey) @usernameField(vpnUser)
-	// TODO(gio): support port allocator
+	sshPrivateKey: string
 }
 
 name: "Cluster Network"
@@ -28,12 +28,12 @@
 			tag: "v1.82.0"
 			pullPolicy: "IfNotPresent"
 		}
-		// portAllocator: {
-		// 	repository: "giolekva"
-		// 	name: "port-allocator"
-		// 	tag: "latest"
-		// 	pullPolicy: "Always"
-		// }
+		portAllocator: {
+			repository: "giolekva"
+			name: "port-allocator"
+			tag: "latest"
+			pullPolicy: "Always"
+		}
 	}
 
 	charts: {
@@ -55,12 +55,12 @@
 			branch: "main"
 			path: "charts/tailscale-proxy"
 		}
-		// portAllocator: {
-		// 	kind: "GitRepository"
-		// 	address: "https://code.v1.dodo.cloud/helm-charts"
-		// 	branch: "main"
-		// 	path: "charts/port-allocator"
-		// }
+		portAllocator: {
+			kind: "GitRepository"
+			address: "https://code.v1.dodo.cloud/helm-charts"
+			branch: "main"
+			path: "charts/port-allocator"
+		}
 	}
 
 	helm: {
@@ -119,20 +119,23 @@
 						}]
   				    }]
 				}
+				tcp: {}
+				udp: {}
 			}
 		}
-		// "port-allocator": {
-		// 	chart: charts.portAllocator
-		// 	values: {
-		// 		repoAddr: release.repoAddr
-		// 		sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-		// 		ingressNginxPath: "\(release.appDir)/resources/ingress-nginx.yaml"
-		// 		image: {
-		// 			repository: images.portAllocator.fullName
-		// 			tag: images.portAllocator.tag
-		// 			pullPolicy: images.portAllocator.pullPolicy
-		// 		}
-		// 	}
-		// }
+		"port-allocator": {
+			chart: charts.portAllocator
+			cluster: null
+			values: {
+				repoAddr: release.repoAddr
+				sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
+				ingressNginxPath: "\(release.appDir)/resources/ingress-nginx.yaml"
+				image: {
+					repository: images.portAllocator.fullName
+					tag: "amd64" // TODO(gio): images.portAllocator.tag
+					pullPolicy: images.portAllocator.pullPolicy
+				}
+			}
+		}
 	}
 }
diff --git a/core/port-allocator/Makefile b/core/port-allocator/Makefile
index eb16feb..3133983 100644
--- a/core/port-allocator/Makefile
+++ b/core/port-allocator/Makefile
@@ -1,4 +1,4 @@
-repo_name ?= dtabidze
+repo_name ?= giolekva
 podman ?= docker
 ifeq ($(podman), podman)
 manifest_dest=docker://docker.io/$(repo_name)/port-allocator:latest
diff --git a/core/port-allocator/go.mod b/core/port-allocator/go.mod
index b0c73b9..dd099e8 100644
--- a/core/port-allocator/go.mod
+++ b/core/port-allocator/go.mod
@@ -6,33 +6,174 @@
 
 require (
 	github.com/giolekva/pcloud/core/installer v0.0.0-00010101000000-000000000000
-	golang.org/x/crypto v0.24.0
+	github.com/go-git/go-billy/v5 v5.5.0
+	golang.org/x/crypto v0.26.0
 )
 
 require (
+	cuelabs.dev/go/oci/ociregistry v0.0.0-20240807094312-a32ad29eed79 // indirect
+	cuelang.org/go v0.10.0 // indirect
 	dario.cat/mergo v1.0.0 // indirect
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+	github.com/BurntSushi/toml v1.3.2 // indirect
+	github.com/MakeNowJust/heredoc v1.0.0 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.2.1 // indirect
+	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+	github.com/Masterminds/squirrel v1.5.4 // indirect
 	github.com/Microsoft/go-winio v0.6.1 // indirect
+	github.com/Microsoft/hcsshim v0.12.2 // indirect
 	github.com/ProtonMail/go-crypto v1.0.0 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/chai2010/gettext-go v1.0.2 // indirect
+	github.com/charmbracelet/keygen v0.5.0 // indirect
 	github.com/cloudflare/circl v1.3.7 // indirect
+	github.com/cockroachdb/apd/v3 v3.2.1 // indirect
+	github.com/containerd/containerd v1.7.14 // indirect
+	github.com/containerd/log v0.1.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/distribution/reference v0.6.0 // indirect
+	github.com/docker/cli v26.0.0+incompatible // indirect
+	github.com/docker/distribution v2.8.3+incompatible // indirect
+	github.com/docker/docker v26.0.0+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.8.1 // indirect
+	github.com/docker/go-connections v0.5.0 // indirect
+	github.com/docker/go-metrics v0.0.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
+	github.com/emicklei/proto v1.13.2 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
+	github.com/evanphx/json-patch v5.9.0+incompatible // indirect
+	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
+	github.com/fatih/color v1.16.0 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fluxcd/helm-controller/api v1.0.1 // indirect
+	github.com/fluxcd/pkg/apis/kustomize v1.5.0 // indirect
+	github.com/fluxcd/pkg/apis/meta v1.5.0 // indirect
+	github.com/go-errors/errors v1.5.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
-	github.com/go-git/go-billy/v5 v5.5.0 // indirect
 	github.com/go-git/go-git/v5 v5.12.0 // indirect
+	github.com/go-gorp/gorp/v3 v3.1.0 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/btree v1.1.2 // indirect
+	github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
+	github.com/gorilla/websocket v1.5.1 // indirect
+	github.com/gosuri/uitable v0.0.4 // indirect
+	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/huandu/xstrings v1.4.0 // indirect
+	github.com/imdario/mergo v0.3.16 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+	github.com/jmoiron/sqlx v1.3.5 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
+	github.com/klauspost/compress v1.17.7 // indirect
+	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
+	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+	github.com/lib/pq v1.10.9 // indirect
+	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-runewidth v0.0.15 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/moby/locker v1.0.1 // indirect
+	github.com/moby/spdystream v0.2.0 // indirect
+	github.com/moby/term v0.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/image-spec v1.1.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_golang v1.19.0 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/common v0.52.2 // indirect
+	github.com/prometheus/procfs v0.13.0 // indirect
+	github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21 // indirect
+	github.com/rubenv/sql-migrate v1.6.1 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/skeema/knownhosts v1.2.2 // indirect
+	github.com/spf13/cast v1.6.0 // indirect
+	github.com/spf13/cobra v1.8.1 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
+	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
+	github.com/xlab/treeprint v1.2.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
+	go.starlark.net v0.0.0-20240329153429-e6e8e7ce1b7a // indirect
 	golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
-	golang.org/x/mod v0.17.0 // indirect
-	golang.org/x/net v0.25.0 // indirect
-	golang.org/x/sync v0.7.0 // indirect
-	golang.org/x/sys v0.22.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/mod v0.20.0 // indirect
+	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/oauth2 v0.22.0 // indirect
+	golang.org/x/sync v0.8.0 // indirect
+	golang.org/x/sys v0.23.0 // indirect
+	golang.org/x/term v0.23.0 // indirect
+	golang.org/x/text v0.17.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
+	golang.org/x/tools v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
+	google.golang.org/grpc v1.63.0 // indirect
+	google.golang.org/protobuf v1.33.0 // indirect
+	gopkg.in/evanphx/json-patch.v5 v5.9.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	helm.sh/helm/v3 v3.14.3 // indirect
+	k8s.io/api v0.30.0 // indirect
+	k8s.io/apiextensions-apiserver v0.30.0 // indirect
+	k8s.io/apimachinery v0.30.0 // indirect
+	k8s.io/apiserver v0.30.0 // indirect
+	k8s.io/cli-runtime v0.29.3 // indirect
+	k8s.io/client-go v0.30.0 // indirect
+	k8s.io/component-base v0.30.0 // indirect
+	k8s.io/klog/v2 v2.120.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20240403164606-bc84c2ddaf99 // indirect
+	k8s.io/kubectl v0.29.3 // indirect
+	k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect
+	oras.land/oras-go v1.2.5 // indirect
+	sigs.k8s.io/controller-runtime v0.18.1 // indirect
+	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	sigs.k8s.io/kustomize/api v0.16.0 // indirect
+	sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/core/port-allocator/go.sum b/core/port-allocator/go.sum
index 51258fc..87eef09 100644
--- a/core/port-allocator/go.sum
+++ b/core/port-allocator/go.sum
@@ -1,32 +1,141 @@
+cuelabs.dev/go/oci/ociregistry v0.0.0-20240807094312-a32ad29eed79 h1:EceZITBGET3qHneD5xowSTY/YHbNybvMWGh62K2fG/M=
+cuelabs.dev/go/oci/ociregistry v0.0.0-20240807094312-a32ad29eed79/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
+cuelang.org/go v0.10.0 h1:Y1Pu4wwga5HkXfLFK1sWAYaSWIBdcsr5Cb5AWj2pOuE=
+cuelang.org/go v0.10.0/go.mod h1:HzlaqqqInHNiqE6slTP6+UtxT9hN6DAzgJgdbNxXvX8=
 dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
+github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
 github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/Microsoft/hcsshim v0.12.2 h1:AcXy+yfRvrx20g9v7qYaJv5Rh+8GaHOS6b8G6Wx/nKs=
+github.com/Microsoft/hcsshim v0.12.2/go.mod h1:RZV12pcHCXQ42XnlQ3pz6FZfmrC1C+R4gaOHhRNML1g=
 github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
 github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
+github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
+github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
+github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
+github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
+github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
+github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
+github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
+github.com/containerd/containerd v1.7.14 h1:H/XLzbnGuenZEGK+v0RkwTdv2u1QFAruMe5N0GNPJwA=
+github.com/containerd/containerd v1.7.14/go.mod h1:YMC9Qt5yzNqXx/fO4j/5yYVIHXSRrlB3H7sxkUTvspg=
+github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
+github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
+github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
 github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
+github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
+github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU=
+github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
+github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
+github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
+github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
+github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fluxcd/helm-controller/api v1.0.1 h1:Gn9qEVuif6D5+gHmVwTEZkR4+nmLOcOhKx4Sw2gL2EA=
+github.com/fluxcd/helm-controller/api v1.0.1/go.mod h1:/6AD5a2qjo/ttxVM8GR33syLZwqigta60DCLdy8GrME=
+github.com/fluxcd/pkg/apis/kustomize v1.5.0 h1:ah4sfqccnio+/5Edz/tVz6LetFhiBoDzXAElj6fFCzU=
+github.com/fluxcd/pkg/apis/kustomize v1.5.0/go.mod h1:nEzhnhHafhWOUUV8VMFLojUOH+HHDEsL75y54mt/c30=
+github.com/fluxcd/pkg/apis/meta v1.5.0 h1:/G82d2Az5D9op3F+wJUpD8jw/eTV0suM6P7+cSURoUM=
+github.com/fluxcd/pkg/apis/meta v1.5.0/go.mod h1:Y3u7JomuuKtr5fvP1Iji2/50FdRe5GcBug2jawNVkdM=
+github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
+github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
 github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
+github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
+github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
@@ -35,15 +144,106 @@
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
 github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
 github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
+github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
+github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
+github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
+github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
+github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
+github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -51,79 +251,257 @@
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
+github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
+github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
+github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
+github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
+github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
+github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
+github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
+github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
 github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
+github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
+github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21 h1:igWZJluD8KtEtAgRyF4x6lqcxDry1ULztksMJh2mnQE=
+github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21/go.mod h1:RMRJLmBOqWacUkmJHRMiPKh1S1m3PA7Zh4W80/kWPpg=
+github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos=
+github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
 github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
+github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.starlark.net v0.0.0-20240329153429-e6e8e7ce1b7a h1:Oe+v9w90BBIxQZ4U39+axR8KxrBbxqnRudPPcBIlP3o=
+go.starlark.net v0.0.0-20240329153429-e6e8e7ce1b7a/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
-golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
 golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
+golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
-golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -131,24 +509,85 @@
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
+golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
+google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v5 v5.9.0 h1:hx1VU2SGj4F8r9b8GUwJLdc8DNO8sy79ZGui0G05GLo=
+gopkg.in/evanphx/json-patch.v5 v5.9.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
+gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+helm.sh/helm/v3 v3.14.3 h1:HmvRJlwyyt9HjgmAuxHbHv3PhMz9ir/XNWHyXfmnOP4=
+helm.sh/helm/v3 v3.14.3/go.mod h1:v6myVbyseSBJTzhmeE39UcPLNv6cQK6qss3dvgAySaE=
+k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
+k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
+k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs=
+k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
+k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
+k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
+k8s.io/apiserver v0.30.0 h1:QCec+U72tMQ+9tR6A0sMBB5Vh6ImCEkoKkTDRABWq6M=
+k8s.io/apiserver v0.30.0/go.mod h1:smOIBq8t0MbKZi7O7SyIpjPsiKJ8qa+llcFCluKyqiY=
+k8s.io/cli-runtime v0.29.3 h1:r68rephmmytoywkw2MyJ+CxjpasJDQY7AGc3XY2iv1k=
+k8s.io/cli-runtime v0.29.3/go.mod h1:aqVUsk86/RhaGJwDhHXH0jcdqBrgdF3bZWk4Z9D4mkM=
+k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
+k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
+k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o=
+k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ=
+k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
+k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20240403164606-bc84c2ddaf99 h1:w6nThEmGo9zcL+xH1Tu6pjxJ3K1jXFW+V0u4peqN8ks=
+k8s.io/kube-openapi v0.0.0-20240403164606-bc84c2ddaf99/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/kubectl v0.29.3 h1:RuwyyIU42MAISRIePaa8Q7A3U74Q9P4MoJbDFz9o3us=
+k8s.io/kubectl v0.29.3/go.mod h1:yCxfY1dbwgVdEt2zkJ6d5NNLOhhWgTyrqACIoFhpdd4=
+k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY=
+k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
+oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
+sigs.k8s.io/controller-runtime v0.18.1 h1:RpWbigmuiylbxOCLy0tGnq1cU1qWPwNIQzoJk+QeJx4=
+sigs.k8s.io/controller-runtime v0.18.1/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g=
+sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c=
+sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0=
+sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/core/port-allocator/main.go b/core/port-allocator/main.go
index 6bc89e4..8b3ab80 100644
--- a/core/port-allocator/main.go
+++ b/core/port-allocator/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
@@ -12,11 +13,13 @@
 	"math/rand"
 	"net/http"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
 
 	"golang.org/x/crypto/ssh"
@@ -36,25 +39,42 @@
 var preOpenPortsBatchSize = flag.Int("pre-open-ports-batch-size", 10, "Number of new ports to open at a time")
 
 type client interface {
-	ReservePort() (int, string, error)
+	ReservePort(remoteProxy bool) (int, string, error)
 	ReleaseReservedPort(port ...int)
 	AddPortForwarding(protocol string, port int, secret, dest string) error
 	RemovePortForwarding(protocol string, port int) error
 }
 
+type Reservation struct {
+	Secret        string `json:"secret"`
+	IsRemoteProxy bool   `json:"isRemoteProxy"`
+}
+
 type repoClient struct {
 	l                     sync.Locker
 	repo                  soft.RepoIO
 	path                  string
 	secretGenerator       SecretGenerator
+	proxyCfg              *installer.NginxProxyConfigurator
 	minPreOpenPorts       int
 	preOpenPortsBatchSize int
 	preOpenPorts          []int
+	proxyPreOpenPorts     []int
 	blocklist             map[int]struct{}
-	reserve               map[int]string
+	reserve               map[int]Reservation
 	availablePorts        []int
 }
 
+func getProxyBackendConfigPath(repo soft.RepoIO, path string) (string, error) {
+	cfgPath := filepath.Join(filepath.Dir(path), "proxy-backend-config.yaml")
+	inp, err := repo.Reader(cfgPath)
+	if err != nil {
+		return "", nil
+	}
+	defer inp.Close()
+	return cfgPath, nil
+}
+
 func newRepoClient(
 	repo soft.RepoIO,
 	path string,
@@ -62,16 +82,29 @@
 	preOpenPortsBatchSize int,
 	secretGenerator SecretGenerator,
 ) (client, error) {
+	proxyCfg, err := getProxyBackendConfigPath(repo, path)
+	if err != nil {
+		return nil, err
+	}
+	var cnc *installer.NginxProxyConfigurator
+	if proxyCfg != "" {
+		cnc = &installer.NginxProxyConfigurator{
+			Repo:        repo,
+			ConfigPath:  proxyCfg,
+			ServicePath: filepath.Join(filepath.Dir(proxyCfg), "proxy-backend-service.yaml"),
+		}
+	}
 	ret := &repoClient{
 		l:                     &sync.Mutex{},
 		repo:                  repo,
 		path:                  path,
 		secretGenerator:       secretGenerator,
+		proxyCfg:              cnc,
 		minPreOpenPorts:       minPreOpenPorts,
 		preOpenPortsBatchSize: preOpenPortsBatchSize,
 		preOpenPorts:          []int{},
 		blocklist:             map[int]struct{}{},
-		reserve:               map[int]string{},
+		reserve:               map[int]Reservation{},
 		availablePorts:        []int{},
 	}
 	st, err := ret.readState(repo)
@@ -103,19 +136,31 @@
 	return ret, nil
 }
 
-func (c *repoClient) ReservePort() (int, string, error) {
+func (c *repoClient) ReservePort(remoteProxy bool) (int, string, error) {
 	c.l.Lock()
 	defer c.l.Unlock()
-	if len(c.preOpenPorts) == 0 {
-		return -1, "", fmt.Errorf("no pre-open ports are available")
+	var port int
+	if !remoteProxy {
+		if len(c.preOpenPorts) == 0 {
+			return -1, "", fmt.Errorf("no pre-open ports are available")
+		}
+		port = c.preOpenPorts[0]
+		c.preOpenPorts = c.preOpenPorts[1:]
+	} else {
+		if c.proxyCfg == nil {
+			return -1, "", fmt.Errorf("does not support TCP/UDP proxy")
+		}
+		if len(c.proxyPreOpenPorts) == 0 {
+			return -1, "", fmt.Errorf("no proxy pre-open ports are available")
+		}
+		port = c.proxyPreOpenPorts[0]
+		c.proxyPreOpenPorts = c.proxyPreOpenPorts[1:]
 	}
-	port := c.preOpenPorts[0]
-	c.preOpenPorts = c.preOpenPorts[1:]
 	secret, err := c.secretGenerator()
 	if err != nil {
 		return -1, "", err
 	}
-	c.reserve[port] = secret
+	c.reserve[port] = Reservation{secret, remoteProxy}
 	return port, secret, nil
 }
 
@@ -127,8 +172,16 @@
 	defer c.l.Unlock()
 	if _, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
 		for _, p := range port {
+			r, ok := c.reserve[p]
+			if !ok {
+				continue
+			}
 			delete(c.reserve, p)
-			c.preOpenPorts = append(c.preOpenPorts, p)
+			if r.IsRemoteProxy {
+				c.proxyPreOpenPorts = append(c.proxyPreOpenPorts, p)
+			} else {
+				c.preOpenPorts = append(c.preOpenPorts, p)
+			}
 		}
 		if err := c.writeState(fs); err != nil {
 			return "", err
@@ -139,30 +192,56 @@
 	}
 }
 
+type oldState struct {
+	PreOpenPorts      []int            `json:"preOpenPorts"`
+	ProxyPreOpenPorts []int            `json:"proxyPreOpenPorts"`
+	Blocklist         map[int]struct{} `json:"blocklist"`
+	Reserve           map[int]string   `json:"reserve"`
+}
+
 type state struct {
-	PreOpenPorts []int            `json:"preOpenPorts"`
-	Blocklist    map[int]struct{} `json:"blocklist"`
-	Reserve      map[int]string   `json:"reserve"`
+	PreOpenPorts      []int               `json:"preOpenPorts"`
+	ProxyPreOpenPorts []int               `json:"proxyPreOpenPorts"`
+	Blocklist         map[int]struct{}    `json:"blocklist"`
+	Reserve           map[int]Reservation `json:"reserve"`
 }
 
 func (c *repoClient) preOpenNewPorts() error {
 	c.l.Lock()
 	defer c.l.Unlock()
-	if len(c.preOpenPorts) >= c.minPreOpenPorts {
+	var ports []int
+	if len(c.preOpenPorts) < c.minPreOpenPorts {
+		for count := c.preOpenPortsBatchSize; count > 0; count-- {
+			if len(c.availablePorts) == 0 {
+				return fmt.Errorf("could not open new port")
+			}
+			r := rand.Intn(len(c.availablePorts))
+			p := c.availablePorts[r]
+			c.availablePorts[r] = c.availablePorts[len(c.availablePorts)-1]
+			c.availablePorts = c.availablePorts[:len(c.availablePorts)-1]
+			ports = append(ports, p)
+			c.preOpenPorts = append(c.preOpenPorts, p)
+			c.blocklist[p] = struct{}{}
+		}
 		return nil
 	}
-	var ports []int
-	for count := c.preOpenPortsBatchSize; count > 0; count-- {
-		if len(c.availablePorts) == 0 {
-			return fmt.Errorf("could not open new port")
+	if c.proxyCfg != nil && len(c.proxyPreOpenPorts) < c.minPreOpenPorts {
+		for count := c.preOpenPortsBatchSize; count > 0; count-- {
+			if len(c.availablePorts) == 0 {
+				return fmt.Errorf("could not open new port")
+			}
+			r := rand.Intn(len(c.availablePorts))
+			p := c.availablePorts[r]
+			c.availablePorts[r] = c.availablePorts[len(c.availablePorts)-1]
+			c.availablePorts = c.availablePorts[:len(c.availablePorts)-1]
+			ports = append(ports, p)
+			c.proxyPreOpenPorts = append(c.proxyPreOpenPorts, p)
+			c.blocklist[p] = struct{}{}
 		}
-		r := rand.Intn(len(c.availablePorts))
-		p := c.availablePorts[r]
-		c.availablePorts[r] = c.availablePorts[len(c.availablePorts)-1]
-		c.availablePorts = c.availablePorts[:len(c.availablePorts)-1]
-		ports = append(ports, p)
-		c.preOpenPorts = append(c.preOpenPorts, p)
-		c.blocklist[p] = struct{}{}
+		return nil
+	}
+	if len(ports) == 0 {
+		return nil
 	}
 	_, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
 		if err := c.writeState(fs); err != nil {
@@ -172,10 +251,17 @@
 		if err != nil {
 			return "", err
 		}
-		svcType, err := extractString(rel, "spec.values.controller.service.type")
+		svcType := ""
+		svcEnabled, err := extractBool(rel, "spec.values.controller.service.enabled")
 		if err != nil {
 			return "", err
 		}
+		if svcEnabled {
+			svcType, err = extractString(rel, "spec.values.controller.service.type")
+			if err != nil {
+				return "", err
+			}
+		}
 		if svcType == "NodePort" {
 			tcp, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
 			if err != nil {
@@ -213,10 +299,31 @@
 	}()
 	c.l.Lock()
 	defer c.l.Unlock()
-	if sec, ok := c.reserve[port]; !ok || sec != secret {
+	r, ok := c.reserve[port]
+	if !ok || r.Secret != secret {
 		return fmt.Errorf("wrong secret")
 	}
 	delete(c.reserve, port)
+	if r.IsRemoteProxy {
+		if c.proxyCfg == nil {
+			return fmt.Errorf("does not support TCP/UDP proxy")
+		}
+		var namespace string
+		var err error
+		switch strings.ToLower(protocol) {
+		case "tcp":
+			if namespace, err = c.proxyCfg.AddProxy(port, dest, installer.ProtocolTCP); err != nil {
+				return err
+			}
+		case "udp":
+			if namespace, err = c.proxyCfg.AddProxy(port, dest, installer.ProtocolUDP); err != nil {
+				return err
+			}
+		default:
+			return fmt.Errorf("unknown protocol: %s", protocol)
+		}
+		dest = fmt.Sprintf("%s/proxy-backend-service:%d", namespace, port)
+	}
 	_, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
 		if err := c.writeState(fs); err != nil {
 			return "", err
@@ -279,10 +386,17 @@
 		default:
 			panic("MUST NOT REACH")
 		}
-		svcType, err := extractString(rel, "spec.values.controller.service.type")
+		svcType := ""
+		svcEnabled, err := extractBool(rel, "spec.values.controller.service.enabled")
 		if err != nil {
 			return "", err
 		}
+		if svcEnabled {
+			svcType, err = extractString(rel, "spec.values.controller.service.type")
+			if err != nil {
+				return "", err
+			}
+		}
 		if svcType == "NodePort" {
 			svcTCP, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
 			if err != nil {
@@ -313,10 +427,27 @@
 		return state{}, err
 	}
 	defer r.Close()
-	var ret state
-	if err := json.NewDecoder(r).Decode(&ret); err != nil {
+	buf, err := io.ReadAll(r)
+	if err != nil {
 		return state{}, err
 	}
+	var ret state
+	if err := json.NewDecoder(bytes.NewReader(buf)).Decode(&ret); err == nil {
+		return ret, nil
+	}
+	var old oldState
+	if err := json.NewDecoder(bytes.NewReader(buf)).Decode(&old); err != nil {
+		return state{}, err
+	}
+	ret = state{
+		PreOpenPorts:      old.PreOpenPorts,
+		ProxyPreOpenPorts: []int{},
+		Blocklist:         old.Blocklist,
+		Reserve:           map[int]Reservation{},
+	}
+	for port, secret := range old.Reserve {
+		ret.Reserve[port] = Reservation{secret, false}
+	}
 	return ret, err
 }
 
@@ -326,7 +457,7 @@
 		return err
 	}
 	defer w.Close()
-	if err := json.NewEncoder(w).Encode(state{c.preOpenPorts, c.blocklist, c.reserve}); err != nil {
+	if err := json.NewEncoder(w).Encode(state{c.preOpenPorts, c.proxyPreOpenPorts, c.blocklist, c.reserve}); err != nil {
 		return err
 	}
 	return err
@@ -422,7 +553,7 @@
 	for _, i := range strings.Split(path, ".") {
 		valM, ok := val.(map[string]any)
 		if !ok {
-			return nil, fmt.Errorf("expected map")
+			return nil, fmt.Errorf("expected map, %s", i)
 		}
 		val, ok = valM[i]
 		if !ok {
@@ -451,7 +582,19 @@
 	}
 	retS, ok := ret.(string)
 	if !ok {
-		return "", fmt.Errorf("expected map")
+		return "", fmt.Errorf("expected string")
+	}
+	return retS, nil
+}
+
+func extractBool(data map[string]any, path string) (bool, error) {
+	ret, err := extractField(data, path)
+	if err != nil {
+		return false, err
+	}
+	retS, ok := ret.(bool)
+	if !ok {
+		return false, fmt.Errorf("expected boolean")
 	}
 	return retS, nil
 }
@@ -496,15 +639,26 @@
 	}
 }
 
+type reserveReq struct {
+	RemoteProxy bool `json:"remoteProxy"`
+}
+
 func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
 		http.Error(w, "only post method is supported", http.StatusBadRequest)
 		return
 	}
+	var req reserveReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		fmt.Println(err.Error())
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	fmt.Printf("%+v\n", req)
 	var port int
 	var secret string
 	var err error
-	if port, secret, err = s.client.ReservePort(); err != nil {
+	if port, secret, err = s.client.ReservePort(req.RemoteProxy); err != nil {
 		fmt.Println(err.Error())
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
diff --git a/core/port-allocator/main_test.go b/core/port-allocator/main_test.go
index 93ae574..1596667 100644
--- a/core/port-allocator/main_test.go
+++ b/core/port-allocator/main_test.go
@@ -26,7 +26,8 @@
 			"values": map[string]any{
 				"controller": map[string]any{
 					"service": map[string]any{
-						"type": "ClusterIP",
+						"enabled": true,
+						"type":    "ClusterIP",
 					},
 				},
 				"tcp": map[string]any{},
@@ -47,7 +48,8 @@
 			"values": map[string]any{
 				"controller": map[string]any{
 					"service": map[string]any{
-						"type": "ClusterIP",
+						"enabled": true,
+						"type":    "ClusterIP",
 					},
 				},
 				"tcp": tcp,
@@ -57,7 +59,7 @@
 	}
 	for i := 0; i < 500; i++ {
 		for _, protocol := range []string{"tcp", "udp"} {
-			port, secret, err := c.ReservePort()
+			port, secret, err := c.ReservePort(false)
 			if err != nil {
 				t.Fatal(err)
 			}