installer: appmanager cmd
diff --git a/core/installer/Makefile b/core/installer/Makefile
index 58a3176..ec40393 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -14,4 +14,4 @@
 	./pcloud create-env --admin-priv-key=/Users/lekva/.ssh/id_rsa --name=foo
 
 rpuppy:
-	./pcloud install --ssh-key=/Users/lekva/.ssh/id_rsa --config=/Users/lekva/dev/src/pcloud/priv/lekva.yaml --app=rpuppy --repo-addr=ssh://localhost:2222/lekva
+	./pcloud install --ssh-key=/Users/lekva/.ssh/id_rsa --app=rpuppy --repo-addr=ssh://localhost:2222/lekva
diff --git a/core/installer/app.go b/core/installer/app.go
index 2f702d8..6f766b9 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -2,17 +2,41 @@
 
 import (
 	"embed"
+	"fmt"
 	"log"
 	"text/template"
 )
 
+//go:embed values-tmpl
+var valuesTmpls embed.FS
+
 type App struct {
 	Name      string
 	Templates []*template.Template
 }
 
-//go:embed values-tmpl
-var valuesTmpls embed.FS
+type AppRepository interface {
+	Find(name string) (*App, error)
+}
+
+type InMemoryAppRepository struct {
+	apps []App
+}
+
+func NewInMemoryAppRepository(apps []App) AppRepository {
+	return &InMemoryAppRepository{
+		apps,
+	}
+}
+
+func (r InMemoryAppRepository) Find(name string) (*App, error) {
+	for _, a := range r.apps {
+		if a.Name == name {
+			return &a, nil
+		}
+	}
+	return nil, fmt.Errorf("Application not found: %s", name)
+}
 
 func CreateAllApps() []App {
 	tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*.yaml")
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 4b66fa5..0678114 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -1,60 +1,74 @@
 package installer
 
 import (
+	"fmt"
 	"io/fs"
+	"net"
+	"time"
 
+	"golang.org/x/crypto/ssh"
 	"golang.org/x/exp/slices"
 
-	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/util"
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 )
 
+const appDirName = "apps"
+const configFileName = "config.yaml"
 const kustomizationFileName = "kustomization.yaml"
 
-type AppRepository interface {
-	Find(name string) (*App, error)
-}
-
 type AppManager struct {
-	fs       billy.Filesystem
-	config   Config
-	appRepo  AppRepository
-	rootKust *Kustomization
+	repo   *git.Repository
+	signer ssh.Signer
 }
 
-func NewAppManager(fs billy.Filesystem, config Config, appRepo AppRepository) (*AppManager, error) {
-	rootKustF, err := fs.Open(kustomizationFileName)
+// func NewAppManager(repo *git.Repository, fs billy.Filesystem, config Config, appRepo AppRepository) (*AppManager, error) {
+func NewAppManager(repo *git.Repository, signer ssh.Signer) (*AppManager, error) {
+	return &AppManager{
+		repo,
+		signer,
+	}, nil
+}
+
+func (m *AppManager) Install(app App) error {
+	wt, err := m.repo.Worktree()
 	if err != nil {
-		return nil, err
+		return err
+	}
+	configF, err := wt.Filesystem.Open(configFileName)
+	if err != nil {
+		return err
+	}
+	defer configF.Close()
+	config, err := ReadConfig(configF)
+	if err != nil {
+		return err
+	}
+	appsRoot, err := wt.Filesystem.Chroot(appDirName)
+	if err != nil {
+		return err
+	}
+	rootKustF, err := appsRoot.Open(kustomizationFileName)
+	if err != nil {
+		return err
 	}
 	defer rootKustF.Close()
 	rootKust, err := ReadKustomization(rootKustF)
 	if err != nil {
-		return nil, err
-	}
-	return &AppManager{
-		fs,
-		config,
-		appRepo,
-		rootKust,
-	}, nil
-}
-
-func (m *AppManager) Install(name string) error {
-	app, err := m.appRepo.Find(name)
-	if err != nil {
-		return nil
-	}
-	if err := util.RemoveAll(m.fs, name); err != nil {
 		return err
 	}
-	if err := m.fs.MkdirAll(name, fs.ModePerm); err != nil {
-		return nil
-	}
-	appRoot, err := m.fs.Chroot(name)
+	appRoot, err := appsRoot.Chroot(app.Name)
 	if err != nil {
 		return err
 	}
+	if err := util.RemoveAll(appRoot, app.Name); err != nil {
+		return err
+	}
+	if err := appRoot.MkdirAll(app.Name, fs.ModePerm); err != nil {
+		return nil
+	}
 	appKust := NewKustomization()
 	for _, t := range app.Templates {
 		out, err := appRoot.Create(t.Name())
@@ -62,7 +76,7 @@
 			return err
 		}
 		defer out.Close()
-		if err := t.Execute(out, m.config); err != nil {
+		if err := t.Execute(out, config); err != nil {
 			return err
 		}
 		appKust.Resources = append(appKust.Resources, t.Name())
@@ -75,14 +89,44 @@
 	if err := appKust.Write(appKustF); err != nil {
 		return err
 	}
-	if slices.Contains(m.rootKust.Resources, name) {
-		return nil
+	if !slices.Contains(rootKust.Resources, app.Name) {
+		rootKust.Resources = append(rootKust.Resources, app.Name)
+		rootKustFW, err := appsRoot.Create(kustomizationFileName)
+		if err != nil {
+			return err
+		}
+		defer rootKustFW.Close()
+		if err := rootKust.Write(rootKustFW); err != nil {
+			return err
+		}
 	}
-	m.rootKust.Resources = append(m.rootKust.Resources, name)
-	rootKustF, err := m.fs.Create(kustomizationFileName)
-	if err != nil {
+	// Commit and push
+	if err := wt.AddGlob("*"); err != nil {
 		return err
 	}
-	defer rootKustF.Close()
-	return m.rootKust.Write(rootKustF)
+	if _, err := wt.Commit(fmt.Sprintf("install: %s", app.Name), &git.CommitOptions{
+		Author: &object.Signature{
+			Name: "pcloud-appmanager",
+			When: time.Now(),
+		},
+	}); err != nil {
+		return err
+	}
+	return m.repo.Push(&git.PushOptions{
+		RemoteName: "origin",
+		Auth:       auth(m.signer),
+	})
+}
+
+func auth(signer ssh.Signer) *gitssh.PublicKeys {
+	return &gitssh.PublicKeys{
+		Signer: signer,
+		HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
+			HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+				// TODO(giolekva): verify server public key
+				// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
+				return nil
+			},
+		},
+	}
 }
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
new file mode 100644
index 0000000..1fc6d64
--- /dev/null
+++ b/core/installer/cmd/app_manager.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+	"os"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/spf13/cobra"
+	"golang.org/x/crypto/ssh"
+)
+
+var appManagerFlags struct {
+	sshKey   string
+	repoAddr string
+}
+
+func appManagerCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "appmanager",
+		RunE: installCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&installFlags.sshKey,
+		"ssh-key",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&installFlags.repoAddr,
+		"repo-addr",
+		"",
+		"",
+	)
+	return cmd
+}
+
+func appManagerCmdRun(cmd *cobra.Command, args []string) error {
+	sshKey, err := os.ReadFile(installFlags.sshKey)
+	if err != nil {
+		return err
+	}
+	signer, err := ssh.ParsePrivateKey(sshKey)
+	if err != nil {
+		return err
+	}
+	repo, err := cloneRepo(installFlags.repoAddr, signer)
+	if err != nil {
+		return err
+	}
+	_, err = installer.NewAppManager(repo, signer)
+	if err != nil {
+		return err
+	}
+	// TODO(gio): start server
+	return nil
+}
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
index b96ac6f..0a58be2 100644
--- a/core/installer/cmd/apps.go
+++ b/core/installer/cmd/apps.go
@@ -1,28 +1,22 @@
 package main
 
 import (
-	"fmt"
-	"io/ioutil"
 	"net"
 	"os"
-	"time"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/go-git/go-git/v5/storage/memory"
 	"github.com/spf13/cobra"
 	"golang.org/x/crypto/ssh"
-	"sigs.k8s.io/yaml"
 )
 
 const appDirName = "apps"
 
 var installFlags struct {
 	sshKey   string
-	config   string
 	appName  string
 	repoAddr string
 }
@@ -39,12 +33,6 @@
 		"",
 	)
 	cmd.Flags().StringVar(
-		&installFlags.config,
-		"config",
-		"",
-		"",
-	)
-	cmd.Flags().StringVar(
 		&installFlags.appName,
 		"app",
 		"",
@@ -59,30 +47,7 @@
 	return cmd
 }
 
-type inMemoryAppRepository struct {
-	apps []installer.App
-}
-
-func NewInMemoryAppRepository(apps []installer.App) installer.AppRepository {
-	return &inMemoryAppRepository{
-		apps,
-	}
-}
-
-func (r inMemoryAppRepository) Find(name string) (*installer.App, error) {
-	for _, a := range r.apps {
-		if a.Name == name {
-			return &a, nil
-		}
-	}
-	return nil, fmt.Errorf("Application not found: %s", name)
-}
-
 func installCmdRun(cmd *cobra.Command, args []string) error {
-	cfg, err := readConfig(installFlags.config)
-	if err != nil {
-		return err
-	}
 	sshKey, err := os.ReadFile(installFlags.sshKey)
 	if err != nil {
 		return err
@@ -95,58 +60,19 @@
 	if err != nil {
 		return err
 	}
-	wt, err := repo.Worktree()
-	if err != nil {
-		return err
-	}
-	appRoot, err := wt.Filesystem.Chroot(appDirName)
-	if err != nil {
-		return err
-	}
 	m, err := installer.NewAppManager(
-		appRoot,
-		cfg,
-		NewInMemoryAppRepository(installer.CreateAllApps()),
+		repo,
+		signer,
 	)
 	if err != nil {
 		return err
 	}
-	if err := m.Install(installFlags.appName); err != nil {
-		return err
-	}
-	if st, err := wt.Status(); err != nil {
-		return err
-	} else {
-		fmt.Printf("%+v\n", st)
-	}
-	wt.AddGlob("*")
-	if st, err := wt.Status(); err != nil {
-		return err
-	} else {
-		fmt.Printf("%+v\n", st)
-	}
-	if _, err := wt.Commit(fmt.Sprintf("install: %s", installFlags.appName), &git.CommitOptions{
-		Author: &object.Signature{
-			Name: "pcloud-appmanager",
-			When: time.Now(),
-		},
-	}); err != nil {
-		return err
-	}
-	return repo.Push(&git.PushOptions{
-		RemoteName: "origin",
-		Auth:       auth(signer),
-	})
-}
-
-func readConfig(config string) (installer.Config, error) {
-	var cfg installer.Config
-	inp, err := ioutil.ReadFile(config)
+	appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	app, err := appRepo.Find(installFlags.appName)
 	if err != nil {
-		return cfg, err
+		return err
 	}
-	err = yaml.UnmarshalStrict(inp, &cfg)
-	return cfg, err
+	return m.Install(*app)
 }
 
 func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
diff --git a/core/installer/config.go b/core/installer/config.go
index 4563f4d..fa171e8 100644
--- a/core/installer/config.go
+++ b/core/installer/config.go
@@ -1,5 +1,12 @@
 package installer
 
+import (
+	"io"
+	"io/ioutil"
+
+	"sigs.k8s.io/yaml"
+)
+
 type Config struct {
 	Values Values `json:"values"`
 }
@@ -22,3 +29,13 @@
 	PiholeOAuth2ClientSecret string `json:"piholeOAuth2ClientSecret,omitempty"`
 	PiholeOAuth2CookieSecret string `json:"piholeOAuth2CookieSecret,omitempty"`
 }
+
+func ReadConfig(r io.Reader) (Config, error) {
+	var cfg Config
+	contents, err := ioutil.ReadAll(r)
+	if err != nil {
+		return cfg, err
+	}
+	err = yaml.UnmarshalStrict(contents, &cfg)
+	return cfg, err
+}