AppManager: Let client override config files during update

Take old input and merge with the new one.

Change-Id: I2d8ad0e5f0cef97a3efa23aa9ca0c7f9163e703b
diff --git a/core/installer/app.go b/core/installer/app.go
index 538e2ff..f8e6d6f 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -482,7 +482,6 @@
 		if vv, ok := v[k]; ok && vv != nil {
 			if mv, ok := val.(map[string]any); ok {
 				// TODO(gio): check that it is actually map
-				fmt.Println(vv)
 				ret[k] = merge(mv, vv.(map[string]any))
 			} else {
 				ret[k] = vv
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index d95e640..e6133c1 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -136,7 +136,7 @@
 	return &cfg, err
 }
 
-func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
+func GetCueAppData(fs soft.RepoFS, dir string, overrides CueAppData) (CueAppData, error) {
 	files, err := fs.ListDir(dir)
 	if err != nil {
 		return nil, err
@@ -151,11 +151,14 @@
 			cfg[f.Name()] = contents
 		}
 	}
+	for k, v := range overrides {
+		cfg[k] = v
+	}
 	return cfg, nil
 }
 
-func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
-	cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id))
+func (m *AppManager) GetInstanceApp(id string, overrides CueAppData) (EnvApp, error) {
+	cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id), overrides)
 	if err != nil {
 		return nil, err
 	}
@@ -615,6 +618,8 @@
 func (m *AppManager) Update(
 	instanceId string,
 	values map[string]any,
+	// TODO(gio): this should not be cue specific
+	overrides CueAppData,
 	opts ...InstallOption,
 ) (ReleaseResources, error) {
 	m.l.Lock()
@@ -627,7 +632,7 @@
 		return ReleaseResources{}, err
 	}
 	instanceDir := filepath.Join(m.appDirRoot, instanceId)
-	app, err := m.GetInstanceApp(instanceId)
+	app, err := m.GetInstanceApp(instanceId, overrides)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -648,7 +653,7 @@
 	if err != nil {
 		return ReleaseResources{}, err
 	}
-	rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), values, renderedCfg.LocalCharts, m.vpnAPIClient)
+	rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -1035,7 +1040,7 @@
 		return ReleaseResources{}, err
 	}
 	instanceDir := filepath.Join("/infrastructure", instanceId)
-	appCfg, err := GetCueAppData(m.repoIO, instanceDir)
+	appCfg, err := GetCueAppData(m.repoIO, instanceDir, nil)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
diff --git a/core/installer/samples/blog.rest b/core/installer/samples/blog.rest
index 0a8606d..2a0b95c 100644
--- a/core/installer/samples/blog.rest
+++ b/core/installer/samples/blog.rest
@@ -1,4 +1,4 @@
-POST http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app
+PUT http://appmanager.hgrz-appmanager.svc.cluster.local/api/dodo-app/dodo-app-snd
 Content-Type: application/json
 
 {
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index 7621213..cb372f3 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -142,6 +142,7 @@
 	r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
 	r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
 	r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
+	r.HandleFunc("/api/dodo-app/{instanceId}", s.handleDodoAppUpdate).Methods(http.MethodPut)
 	r.HandleFunc("/api/dodo-app", s.handleDodoAppInstall).Methods(http.MethodPost)
 	r.HandleFunc("/clusters/{cluster}/servers/{server}/remove", s.handleClusterRemoveServer).Methods(http.MethodPost)
 	r.HandleFunc("/clusters/{cluster}/servers", s.handleClusterAddServer).Methods(http.MethodPost)
@@ -216,6 +217,37 @@
 	}
 }
 
+func (s *Server) handleDodoAppUpdate(w http.ResponseWriter, r *http.Request) {
+	instanceId, ok := mux.Vars(r)["instanceId"]
+	if !ok {
+		http.Error(w, "missing instance id", http.StatusBadRequest)
+	}
+	var req dodoAppInstallReq
+	// TODO(gio): validate that no internal fields are overridden by request
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	clusters, err := s.m.GetClusters()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	req.Config["clusters"] = installer.ToAccessConfigs(clusters)
+	var cfg bytes.Buffer
+	if err := json.NewEncoder(&cfg).Encode(req.Config); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	overrides := installer.CueAppData{
+		"app.cue": cfg.Bytes(),
+	}
+	// TODO(gio): return monitoring info
+	if _, err := s.m.Update(instanceId, nil, overrides); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+}
+
 func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
 	env, err := s.m.Config()
 	if err != nil {
@@ -424,7 +456,7 @@
 		}
 		tid = t.id + 1
 	}
-	rr, err := s.m.Update(slug, values)
+	rr, err := s.m.Update(slug, values, nil)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -583,7 +615,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	a, err := s.m.GetInstanceApp(instance.Id)
+	a, err := s.m.GetInstanceApp(instance.Id, nil)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return