AppManager: Sync installations

Change-Id: I942de06226e962be996a69f3522f3e510cacd252
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 6cf1da1..a7c23f0 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -11,6 +11,7 @@
 	"path"
 	"path/filepath"
 	"strings"
+	"sync"
 
 	gio "github.com/giolekva/pcloud/core/installer/io"
 	"github.com/giolekva/pcloud/core/installer/soft"
@@ -29,6 +30,7 @@
 var ErrorNotFound = errors.New("not found")
 
 type AppManager struct {
+	l          sync.Locker
 	repoIO     soft.RepoIO
 	nsc        NamespaceCreator
 	jc         JobCreator
@@ -44,6 +46,7 @@
 	appDirRoot string,
 ) (*AppManager, error) {
 	return &AppManager{
+		&sync.Mutex{},
 		repoIO,
 		nsc,
 		jc,
@@ -394,6 +397,14 @@
 	values map[string]any,
 	opts ...InstallOption,
 ) (ReleaseResources, error) {
+	o := &installOptions{}
+	for _, i := range opts {
+		i(o)
+	}
+	if !o.NoLock {
+		m.l.Lock()
+		defer m.l.Unlock()
+	}
 	portFields := findPortFields(app.Schema())
 	fakeReservations := map[string]reservePortResp{}
 	for i, f := range portFields {
@@ -402,10 +413,6 @@
 	if err := setPortFields(values, fakeReservations); err != nil {
 		return ReleaseResources{}, err
 	}
-	o := &installOptions{}
-	for _, i := range opts {
-		i(o)
-	}
 	appDir = filepath.Clean(appDir)
 	if !o.NoPull {
 		if err := m.repoIO.Pull(); err != nil {
@@ -544,6 +551,8 @@
 	values map[string]any,
 	opts ...InstallOption,
 ) (ReleaseResources, error) {
+	m.l.Lock()
+	defer m.l.Unlock()
 	if err := m.repoIO.Pull(); err != nil {
 		return ReleaseResources{}, err
 	}
@@ -584,6 +593,8 @@
 }
 
 func (m *AppManager) Remove(instanceId string) error {
+	m.l.Lock()
+	defer m.l.Unlock()
 	if err := m.repoIO.Pull(); err != nil {
 		return err
 	}