AppManager: Add API endpoint to install dodo app

Refactors cue definitions.

Next steps:
* Needs some cleanup, namespace is hard coded ...
* Maybe merge with regular install API
* Support exposing ports across clusters

Change-Id: Ibfc3c3f742b61f2c5874012fe6c77b958eae81d9
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index 5e1e06b..b3a0883 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -1,6 +1,7 @@
 package appmanager
 
 import (
+	"bytes"
 	"context"
 	"embed"
 	"encoding/json"
@@ -9,6 +10,7 @@
 	"html/template"
 	"net"
 	"net/http"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"sync"
@@ -138,6 +140,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", 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)
 	r.HandleFunc("/clusters/{name}", s.handleCluster).Methods(http.MethodGet)
@@ -154,6 +157,47 @@
 	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 }
 
+type dodoAppInstallReq struct {
+	Id            string         `json:"id"`
+	SSHPrivateKey string         `json:"sshPrivateKey"`
+	Config        map[string]any `json:"config"`
+}
+
+func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
+	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
+	}
+	app, err := installer.NewDodoApp(cfg.Bytes())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	appDir := filepath.Join("/dodo-app", req.Id)
+	namespace := "dodo-app-test" // TODO(gio)
+	if _, err := s.m.Install(app, req.Id, appDir, namespace, map[string]any{
+		"managerAddr":   "", // TODO(gio)
+		"appId":         req.Id,
+		"sshPrivateKey": req.SSHPrivateKey,
+	}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func (s *Server) handleNetworks(w http.ResponseWriter, r *http.Request) {
 	env, err := s.m.Config()
 	if err != nil {