DodoApp: Use one network per external customer

Change-Id: I09864ad24a223b451c5877ad08bbc8960c80d41c
diff --git a/apps/app-runner/Dockerfile b/apps/app-runner/Dockerfile.golang.1.20.0
similarity index 68%
copy from apps/app-runner/Dockerfile
copy to apps/app-runner/Dockerfile.golang.1.20.0
index 964cd31..16f484f 100644
--- a/apps/app-runner/Dockerfile
+++ b/apps/app-runner/Dockerfile.golang.1.20.0
@@ -1,4 +1,4 @@
-FROM golang:1.22.0-bookworm
+FROM golang:1.20.0-alpine3.17
 
 ARG TARGETARCH
 
diff --git a/apps/app-runner/Dockerfile b/apps/app-runner/Dockerfile.golang.1.22.0
similarity index 68%
rename from apps/app-runner/Dockerfile
rename to apps/app-runner/Dockerfile.golang.1.22.0
index 964cd31..af75fe6 100644
--- a/apps/app-runner/Dockerfile
+++ b/apps/app-runner/Dockerfile.golang.1.22.0
@@ -1,4 +1,4 @@
-FROM golang:1.22.0-bookworm
+FROM golang:1.22.0-alpine3.19
 
 ARG TARGETARCH
 
diff --git a/apps/app-runner/Makefile b/apps/app-runner/Makefile
index 8a8936a..11a659d 100644
--- a/apps/app-runner/Makefile
+++ b/apps/app-runner/Makefile
@@ -1,7 +1,9 @@
 repo_name ?= giolekva
 podman ?= docker
 ifeq ($(podman), podman)
-manifest_dest=docker://docker.io/$(repo_name)/app-runner:golang-1.22.0
+manifest_dest_golang_1_22_0=docker://docker.io/$(repo_name)/app-runner:golang-1.22.0
+manifest_dest_golang_1_20_0=docker://docker.io/$(repo_name)/app-runner:golang-1.20.0
+manifest_dest_hugo_latest=docker://docker.io/$(repo_name)/app-runner:hugo-latest
 endif
 
 clean:
@@ -21,19 +23,36 @@
 build_amd64:
 	/usr/local/go/bin/go build -o app-runner_amd64 *.go
 
-push_arm64: clean build_arm64
-	$(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:golang-1.22.0-arm64 .
+# Golang 1.22.0
+
+push_golang_1_22_0_arm64: clean build_arm64
+	$(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:golang-1.22.0-arm64 -f Dockerfile.golang.1.22.0 .
 	$(podman) push $(repo_name)/app-runner:golang-1.22.0-arm64
 
-push_amd64: clean build_amd64
-	$(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:golang-1.22.0-amd64 .
+push_golang_1_22_0_amd64: clean build_amd64
+	$(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:golang-1.22.0-amd64 -f Dockerfile.golang.1.22.0 .
 	$(podman) push $(repo_name)/app-runner:golang-1.22.0-amd64
 
-push: push_arm64 push_amd64
+push_golang_1_22_0: push_golang_1_22_0_arm64 push_golang_1_22_0_amd64
 	$(podman) manifest create $(repo_name)/app-runner:golang-1.22.0 $(repo_name)/app-runner:golang-1.22.0-arm64 $(repo_name)/app-runner:golang-1.22.0-amd64
-	$(podman) manifest push $(repo_name)/app-runner:golang-1.22.0 $(manifest_dest)
+	$(podman) manifest push $(repo_name)/app-runner:golang-1.22.0 $(manifest_dest_golang_1_22_0)
 	$(podman) manifest rm $(repo_name)/app-runner:golang-1.22.0
 
+# Golang 1.20.0
+
+push_golang_1_20_0_arm64: clean build_arm64
+	$(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:golang-1.20.0-arm64 -f Dockerfile.golang.1.20.0 .
+	$(podman) push $(repo_name)/app-runner:golang-1.20.0-arm64
+
+push_golang_1_20_0_amd64: clean build_amd64
+	$(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:golang-1.20.0-amd64 -f Dockerfile.golang.1.20.0 .
+	$(podman) push $(repo_name)/app-runner:golang-1.20.0-amd64
+
+push_golang_1_20_0: push_golang_1_20_0_arm64 push_golang_1_20_0_amd64
+	$(podman) manifest create $(repo_name)/app-runner:golang-1.20.0 $(repo_name)/app-runner:golang-1.20.0-arm64 $(repo_name)/app-runner:golang-1.20.0-amd64
+	$(podman) manifest push $(repo_name)/app-runner:golang-1.20.0 $(manifest_dest_golang_1_20_0)
+	$(podman) manifest rm $(repo_name)/app-runner:golang-1.20.0
+
 # Hugo
 
 push_hugo_arm64: clean build_arm64
@@ -46,5 +65,8 @@
 
 push_hugo: push_hugo_arm64 push_hugo_amd64
 	$(podman) manifest create $(repo_name)/app-runner:hugo-latest $(repo_name)/app-runner:hugo-latest-arm64 $(repo_name)/app-runner:hugo-latest-amd64
-	$(podman) manifest push $(repo_name)/app-runner:hugo-latest $(manifest_dest)
+	$(podman) manifest push $(repo_name)/app-runner:hugo-latest $(manifest_dest_hugo_latest)
 	$(podman) manifest rm $(repo_name)/app-runner:hugo-latest
+
+# all
+push: push_golang_1_22_0 push_golang_1_20_0 push_hugo
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index 5fc6e3e..f6d9c43 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -195,6 +195,7 @@
 		nsc,
 		jc,
 		env,
+		!dodoAppFlags.external,
 	)
 	if err != nil {
 		return err
diff --git a/core/installer/welcome/app-tmpl/golang-1.20.0/go.mod.gotmpl b/core/installer/welcome/app-tmpl/golang-1.20.0/go.mod.gotmpl
index cafcee0..190c9ac 100755
--- a/core/installer/welcome/app-tmpl/golang-1.20.0/go.mod.gotmpl
+++ b/core/installer/welcome/app-tmpl/golang-1.20.0/go.mod.gotmpl
@@ -1,3 +1,3 @@
 module dodo.app
 
-go 1.20.0
\ No newline at end of file
+go 1.20
\ No newline at end of file
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index e6f3d37..462a95d 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -33,7 +33,7 @@
 
 const (
 	ConfigRepoName = "config"
-	namespacesFile = "/namespaces.json"
+	appConfigsFile = "/apps.json"
 	loginPath      = "/login"
 	logoutPath     = "/logout"
 	staticPath     = "/static"
@@ -91,9 +91,15 @@
 	nsc               installer.NamespaceCreator
 	jc                installer.JobCreator
 	workers           map[string]map[string]struct{}
-	appNs             map[string]string
+	appConfigs        map[string]appConfig
 	tmplts            dodoAppTmplts
 	appTmpls          AppTmplStore
+	allowNetworkReuse bool
+}
+
+type appConfig struct {
+	Namespace string `json:"namespace"`
+	Network   string `json:"network"`
 }
 
 // TODO(gio): Initialize appNs on startup
@@ -113,6 +119,7 @@
 	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
+	allowNetworkReuse bool,
 ) (*DodoAppServer, error) {
 	tmplts, err := parseTemplatesDodoApp(dodoAppTmplFS)
 	if err != nil {
@@ -144,18 +151,19 @@
 		nsc,
 		jc,
 		map[string]map[string]struct{}{},
-		map[string]string{},
+		map[string]appConfig{},
 		tmplts,
 		appTmpls,
+		allowNetworkReuse,
 	}
 	config, err := client.GetRepo(ConfigRepoName)
 	if err != nil {
 		return nil, err
 	}
-	r, err := config.Reader(namespacesFile)
+	r, err := config.Reader(appConfigsFile)
 	if err == nil {
 		defer r.Close()
-		if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
+		if err := json.NewDecoder(r).Decode(&s.appConfigs); err != nil {
 			return nil, err
 		}
 	} else if !errors.Is(err, fs.ErrNotExist) {
@@ -432,7 +440,7 @@
 		if err != nil {
 			return
 		}
-		if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
+		if err := s.updateDodoApp(req.Repository.Name, s.appConfigs[req.Repository.Name].Namespace, networks); err != nil {
 			if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
 				fmt.Printf("Error: %s\n", err.Error())
 				return
@@ -526,7 +534,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.CreateApp(user, appName, appType, adminPublicKey, network, subdomain); err != nil {
+	if err := s.createApp(user, appName, appType, network, subdomain); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -546,6 +554,7 @@
 }
 
 func (s *DodoAppServer) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
 	var req apiCreateAppReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -585,7 +594,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.CreateApp(user, appName, req.AppType, req.AdminPublicKey, req.Network, req.Subdomain); err != nil {
+	if err := s.createApp(user, appName, req.AppType, req.Network, req.Subdomain); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -593,17 +602,32 @@
 		AppName:  appName,
 		Password: password,
 	}
-	w.Header().Set("Access-Control-Allow-Origin", "*")
 	if err := json.NewEncoder(w).Encode(resp); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 }
 
-func (s *DodoAppServer) CreateApp(user, appName, appType, adminPublicKey, network, subdomain string) error {
+func (s *DodoAppServer) isNetworkUseAllowed(network string) bool {
+	if s.allowNetworkReuse {
+		return true
+	}
+	for _, cfg := range s.appConfigs {
+		if strings.ToLower(cfg.Network) == network {
+			return false
+		}
+	}
+	return true
+}
+
+func (s *DodoAppServer) createApp(user, appName, appType, network, subdomain string) error {
 	s.l.Lock()
 	defer s.l.Unlock()
 	fmt.Printf("Creating app: %s\n", appName)
+	network = strings.ToLower(network)
+	if !s.isNetworkUseAllowed(network) {
+		return fmt.Errorf("network already used: %s", network)
+	}
 	if ok, err := s.client.RepoExists(appName); err != nil {
 		return err
 	} else if ok {
@@ -613,7 +637,7 @@
 	if err != nil {
 		return err
 	}
-	n, ok := installer.NetworkMap(networks)[strings.ToLower(network)]
+	n, ok := installer.NetworkMap(networks)[network]
 	if !ok {
 		return fmt.Errorf("network not found: %s\n", network)
 	}
@@ -638,26 +662,26 @@
 		return err
 	}
 	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
-	s.appNs[appName] = namespace
+	s.appConfigs[appName] = appConfig{namespace, network}
 	if err := s.updateDodoApp(appName, namespace, networks); err != nil {
 		return err
 	}
-	repo, err := s.client.GetRepo(ConfigRepoName)
+	configRepo, err := s.client.GetRepo(ConfigRepoName)
 	if err != nil {
 		return err
 	}
 	hf := installer.NewGitHelmFetcher()
-	m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
+	m, err := installer.NewAppManager(configRepo, s.nsc, s.jc, hf, "/")
 	if err != nil {
 		return err
 	}
-	if err := repo.Do(func(fs soft.RepoFS) (string, error) {
-		w, err := fs.Writer(namespacesFile)
+	if err := configRepo.Do(func(fs soft.RepoFS) (string, error) {
+		w, err := fs.Writer(appConfigsFile)
 		if err != nil {
 			return "", err
 		}
 		defer w.Close()
-		if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
+		if err := json.NewEncoder(w).Encode(s.appConfigs); err != nil {
 			return "", err
 		}
 		if _, err := m.Install(
@@ -815,6 +839,9 @@
 }
 
 func (s *DodoAppServer) handleAPIPublicData(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	s.l.Lock()
+	defer s.l.Unlock()
 	networks, err := s.getNetworks("")
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -822,7 +849,9 @@
 	}
 	var ret publicData
 	for _, n := range networks {
-		ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
+		if s.isNetworkUseAllowed(strings.ToLower(n.Name)) {
+			ret.Networks = append(ret.Networks, publicNetworkData{n.Name, n.Domain})
+		}
 	}
 	for _, t := range s.appTmpls.Types() {
 		ret.Types = append(ret.Types, strings.ReplaceAll(t, "-", ":"))