AppManager: Handle new port forwards during app update

Change-Id: I72a4c5b7ec4bd5ba6ddd32cd3f33dce023d7d9ea
diff --git a/core/installer/app.go b/core/installer/app.go
index f830455..0b59ca1 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -494,7 +494,7 @@
 	for k, v := range v {
 		if v != nil {
 			if _, ok := d[k]; !ok {
-				d[k] = v
+				ret[k] = v
 			}
 		}
 	}
@@ -519,7 +519,6 @@
 	if err != nil {
 		return EnvAppRendered{}, err
 	}
-	// return EnvAppRendered{}, fmt.Errorf("asdasd")
 	if charts == nil {
 		charts = make(map[string]helmv2.HelmChartTemplateSpec)
 	}
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 {
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 7a7bf7a..0838f14 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -6,6 +6,7 @@
 	"net"
 	"testing"
 
+	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/errors"
 )
 
@@ -644,3 +645,63 @@
 		t.Log(string(r))
 	}
 }
+
+func TestUpdateUsesPreviousValues(t *testing.T) {
+	contents := `
+input: {
+  port: int | *5
+}
+
+out: {
+  openPort: [{
+    name: "api"
+    network: networks["private"]
+    port: input.port
+    service: {
+      name: "app"
+      port: 8080
+    }
+  }]
+}
+
+input: {
+  port: 10
+}
+`
+	app, err := NewCueEnvApp(CueAppData{
+		"base.cue":   []byte(cueBaseConfig),
+		"app.cue":    []byte(contents),
+		"global.cue": []byte(cueEnvAppGlobal),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	a, ok := app.(cueEnvApp)
+	if !ok {
+		t.Fatal("expected cue app")
+	}
+	defaults, err := ExtractDefaultValues(a.cfg.LookupPath(cue.ParsePath("input")))
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Log(defaults)
+	if defaults.(map[string]any)["port"].(int64) != 10 {
+		t.Fatal("port")
+	}
+}
+
+func TestMerge(t *testing.T) {
+	x := map[string]any{
+		"key": map[string]any{
+			"pub":  1,
+			"priv": 2,
+		},
+	}
+	y := map[string]any{
+		"foo": 3,
+	}
+	z := merge(x, y)
+	if _, ok := z["foo"]; !ok {
+		t.Fatal(z)
+	}
+}
diff --git a/core/installer/schema.go b/core/installer/schema.go
index fb5d305..c7f7196 100644
--- a/core/installer/schema.go
+++ b/core/installer/schema.go
@@ -383,7 +383,7 @@
 		// TODO(gio): handle numbers
 		return nil, fmt.Errorf("implement: %s", v)
 	case cue.IntKind:
-		if d, ok := v.Default(); ok {
+		if d, ok := v.Default(); ok || d.IsConcrete() {
 			return d.Int64()
 		}
 	case cue.ListKind: