AppManager: Handle new port forwards during app update

Change-Id: I72a4c5b7ec4bd5ba6ddd32cd3f33dce023d7d9ea
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 7995a41..d0faed4 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -10,6 +10,7 @@
 	"net/http"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 	"sync"
 
@@ -632,6 +633,9 @@
 	overrides CueAppData,
 	opts ...InstallOption,
 ) (ReleaseResources, error) {
+	if values == nil {
+		values = map[string]any{}
+	}
 	m.l.Lock()
 	defer m.l.Unlock()
 	if err := m.repo.Pull(); err != nil {
@@ -642,10 +646,29 @@
 		return ReleaseResources{}, err
 	}
 	instanceDir := filepath.Join(m.appDirRoot, instanceId)
-	app, err := m.GetInstanceApp(instanceId, overrides)
+	oldApp, err := m.GetInstanceApp(instanceId, nil)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
+	newApp, err := m.GetInstanceApp(instanceId, overrides)
+	if err != nil {
+		return ReleaseResources{}, err
+	}
+	oldPorts := findPortFields(oldApp.Schema())
+	newPorts := findPortFields(newApp.Schema())
+	portFields := []string{}
+	for _, np := range newPorts {
+		if !slices.Contains(oldPorts, np) {
+			portFields = append(portFields, np)
+		}
+	}
+	fakeReservations := map[string]reservePortResp{}
+	for i, f := range portFields {
+		fakeReservations[f] = reservePortResp{Port: i}
+	}
+	if err := setPortFields(values, fakeReservations); err != nil {
+		return ReleaseResources{}, err
+	}
 	instanceConfigPath := filepath.Join(instanceDir, "config.json")
 	config, err := m.appConfig(instanceConfigPath)
 	if err != nil {
@@ -663,7 +686,36 @@
 	if err != nil {
 		return ReleaseResources{}, err
 	}
-	rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
+	rendered, err := newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
+	if err != nil {
+		return ReleaseResources{}, err
+	}
+	reservators := map[string]reservePortInfo{}
+	allocators := map[string]string{}
+	for _, pf := range rendered.Ports {
+		found := false
+		for _, fr := range fakeReservations {
+			if fr.Port == pf.Port {
+				found = true
+			}
+		}
+		if !found {
+			continue
+		}
+		reservators[portFields[pf.Port]] = reservePortInfo{
+			reserveAddr: pf.Network.ReservePortAddr,
+			RemoteProxy: pf.Cluster != "",
+		}
+		allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
+	}
+	portReservations, err := reservePorts(reservators)
+	if err != nil {
+		return ReleaseResources{}, err
+	}
+	if err := setPortFields(values, portReservations); err != nil {
+		return ReleaseResources{}, err
+	}
+	rendered, err = newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -685,6 +737,22 @@
 	if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
 		return ReleaseResources{}, err
 	}
+	toOpen := []PortForward{}
+	for _, op := range rendered.Ports {
+		found := false
+		for _, rp := range portReservations {
+			if rp.Port == op.Port {
+				found = true
+				break
+			}
+		}
+		if !found {
+			toOpen = append(toOpen, op)
+		}
+	}
+	if err := openPorts(toOpen, portReservations, allocators, config.Release.Namespace); err != nil {
+		return ReleaseResources{}, err
+	}
 	for _, ocp := range renderedCfg.Out.ClusterProxy {
 		found := false
 		for _, ncp := range rendered.ClusterProxies {