DodoApp: Persist app namespaces in the config repository

Change-Id: I6bb6231ff63a4cfa8b66aa75c3d4cc1d9985d389
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index d3b64ab..0930356 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -310,6 +310,9 @@
 	if o.Force {
 		dopts = append(dopts, soft.WithForce())
 	}
+	if o.NoLock {
+		dopts = append(dopts, soft.WithNoLock())
+	}
 	return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
 		if err := r.RemoveDir(appDir); err != nil {
 			return "", err
@@ -596,6 +599,7 @@
 	LG                   LocalChartGenerator
 	FetchContainerImages bool
 	Force                bool
+	NoLock               bool
 }
 
 type InstallOption func(*installOptions)
@@ -636,6 +640,12 @@
 	}
 }
 
+func WithNoLock() InstallOption {
+	return func(o *installOptions) {
+		o.NoLock = true
+	}
+}
+
 // InfraAppmanager
 
 type InfraAppManager struct {
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index 5d54147..78c88d2 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -106,7 +106,7 @@
 	if err != nil {
 		return err
 	}
-	s := welcome.NewDodoAppServer(
+	s, err := welcome.NewDodoAppServer(
 		dodoAppFlags.port,
 		dodoAppFlags.self,
 		string(sshKey),
@@ -117,6 +117,9 @@
 		jc,
 		env,
 	)
+	if err != nil {
+		return err
+	}
 	if dodoAppFlags.appAdminKey != "" {
 		if err := s.CreateApp("app", dodoAppFlags.appAdminKey); err != nil {
 			return err
diff --git a/core/installer/soft/repoio.go b/core/installer/soft/repoio.go
index dcf897d..872a5cf 100644
--- a/core/installer/soft/repoio.go
+++ b/core/installer/soft/repoio.go
@@ -40,10 +40,17 @@
 	NoCommit bool
 	Force    bool
 	ToBranch string
+	NoLock   bool
 }
 
 type DoOption func(*doOptions)
 
+func WithNoLock() DoOption {
+	return func(o *doOptions) {
+		o.NoLock = true
+	}
+}
+
 func WithNoPull() DoOption {
 	return func(o *doOptions) {
 		o.NoPull = true
@@ -108,7 +115,7 @@
 }
 
 func (r *repoFS) Writer(path string) (io.WriteCloser, error) {
-	if err := r.fs.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
+	if err := r.CreateDir(filepath.Dir(path)); err != nil {
 		return nil, err
 	}
 	return r.fs.Create(path)
@@ -224,32 +231,34 @@
 }
 
 func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
-	r.l.Lock()
-	defer r.l.Unlock()
 	o := &doOptions{}
 	for _, i := range opts {
 		i(o)
 	}
+	if o.NoLock {
+		r.l.Lock()
+		defer r.l.Unlock()
+	}
 	if !o.NoPull {
 		if err := r.pullWithoutLock(); err != nil {
 			return err
 		}
 	}
-	if msg, err := op(r); err != nil {
+	msg, err := op(r)
+	if err != nil {
 		return err
-	} else {
-		if !o.NoCommit {
-			popts := []PushOption{}
-			if o.Force {
-				popts = append(popts, PushWithForce())
-			}
-			if o.ToBranch != "" {
-				popts = append(popts, WithToBranch(o.ToBranch))
-			}
-			return r.CommitAndPush(msg, popts...)
-		}
 	}
-	return nil
+	if o.NoCommit {
+		return nil
+	}
+	popts := []PushOption{}
+	if o.Force {
+		popts = append(popts, PushWithForce())
+	}
+	if o.ToBranch != "" {
+		popts = append(popts, WithToBranch(o.ToBranch))
+	}
+	return r.CommitAndPush(msg, popts...)
 }
 
 func auth(signer ssh.Signer) *gitssh.PublicKeys {
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index f903d9b..8cfa72f 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -2,10 +2,13 @@
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"net/http"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/giolekva/pcloud/core/installer"
@@ -14,7 +17,13 @@
 	"github.com/gorilla/mux"
 )
 
+const (
+	configRepoName = "config"
+	namespacesFile = "/namespaces.json"
+)
+
 type DodoAppServer struct {
+	l                sync.Locker
 	port             int
 	self             string
 	sshKey           string
@@ -39,8 +48,16 @@
 	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
-) *DodoAppServer {
-	return &DodoAppServer{
+) (*DodoAppServer, error) {
+	if ok, err := client.RepoExists(configRepoName); err != nil {
+		return nil, err
+	} else if !ok {
+		if err := client.AddRepository(configRepoName); err != nil {
+			return nil, err
+		}
+	}
+	s := &DodoAppServer{
+		&sync.Mutex{},
 		port,
 		self,
 		sshKey,
@@ -53,6 +70,20 @@
 		map[string]map[string]struct{}{},
 		map[string]string{},
 	}
+	config, err := client.GetRepo(configRepoName)
+	if err != nil {
+		return nil, err
+	}
+	r, err := config.Reader(namespacesFile)
+	if err == nil {
+		defer r.Close()
+		if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
+			return nil, err
+		}
+	} else if !errors.Is(err, fs.ErrNotExist) {
+		return nil, err
+	}
+	return s, nil
 }
 
 func (s *DodoAppServer) Start() error {
@@ -82,7 +113,7 @@
 		fmt.Println(err)
 		return
 	}
-	if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, "config") {
+	if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, configRepoName) {
 		return
 	}
 	go func() {
@@ -148,6 +179,8 @@
 }
 
 func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) error {
+	s.l.Lock()
+	defer s.l.Unlock()
 	fmt.Printf("Creating app: %s\n", appName)
 	if ok, err := s.client.RepoExists(appName); err != nil {
 		return err
@@ -179,14 +212,7 @@
 	if err := s.updateDodoApp(appName, namespace); err != nil {
 		return err
 	}
-	if ok, err := s.client.RepoExists("config"); err != nil {
-		return err
-	} else if !ok {
-		if err := s.client.AddRepository("config"); err != nil {
-			return err
-		}
-	}
-	repo, err := s.client.GetRepo("config")
+	repo, err := s.client.GetRepo(configRepoName)
 	if err != nil {
 		return err
 	}
@@ -195,11 +221,33 @@
 	if err != nil {
 		return err
 	}
-	if _, err := m.Install(app, appName, "/"+appName, namespace, map[string]any{
-		"repoAddr":         s.client.GetRepoAddress(appName),
-		"repoHost":         strings.Split(s.client.Address(), ":")[0],
-		"gitRepoPublicKey": s.gitRepoPublicKey,
-	}, installer.WithConfig(&s.env)); err != nil {
+	if err := repo.Do(func(fs soft.RepoFS) (string, error) {
+		w, err := fs.Writer(namespacesFile)
+		if err != nil {
+			return "", err
+		}
+		defer w.Close()
+		if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
+			return "", err
+		}
+		if _, err := m.Install(
+			app,
+			appName,
+			"/"+appName,
+			namespace,
+			map[string]any{
+				"repoAddr":         s.client.GetRepoAddress(appName),
+				"repoHost":         strings.Split(s.client.Address(), ":")[0],
+				"gitRepoPublicKey": s.gitRepoPublicKey,
+			},
+			installer.WithConfig(&s.env),
+			installer.WithNoPublish(),
+			installer.WithNoLock(),
+		); err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("Installed app: %s", appName), nil
+	}); err != nil {
 		return err
 	}
 	cfg, err := m.FindInstance(appName)