installer: clean up codebase

* introduce helper soft package to work with SoftServe
* move commands to cmd/
diff --git a/core/installer/cmd/apps.go b/core/installer/cmd/apps.go
new file mode 100644
index 0000000..eb49736
--- /dev/null
+++ b/core/installer/cmd/apps.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/spf13/cobra"
+	"sigs.k8s.io/yaml"
+)
+
+var installFlags struct {
+	config    string
+	appName   string
+	outputDir string
+}
+
+func installCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "install",
+		RunE: installCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&installFlags.config,
+		"config",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&installFlags.appName,
+		"app",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&installFlags.outputDir,
+		"output-dir",
+		"",
+		"",
+	)
+	return cmd
+}
+
+func installCmdRun(cmd *cobra.Command, args []string) error {
+	cfg, err := readConfig(installFlags.config)
+	if err != nil {
+		return err
+	}
+	apps := installer.CreateAllApps()
+	for _, a := range apps {
+		if a.Name == installFlags.appName {
+			for _, t := range a.Templates {
+				out, err := os.Create(filepath.Join(installFlags.outputDir, t.Name()))
+				if err != nil {
+					return err
+				}
+				defer out.Close()
+				if err := t.Execute(out, cfg); err != nil {
+					return err
+				}
+			}
+			break
+		}
+	}
+	return nil
+}
+
+func readConfig(config string) (installer.Config, error) {
+	var cfg installer.Config
+	inp, err := ioutil.ReadFile(config)
+	if err != nil {
+		return cfg, err
+	}
+	err = yaml.UnmarshalStrict(inp, &cfg)
+	return cfg, err
+}
diff --git a/core/installer/cmd/bootstrap.go b/core/installer/cmd/bootstrap.go
new file mode 100644
index 0000000..151dd90
--- /dev/null
+++ b/core/installer/cmd/bootstrap.go
@@ -0,0 +1,199 @@
+package main
+
+import (
+	"context"
+	"crypto/ed25519"
+	"crypto/rand"
+	"crypto/x509"
+	_ "embed"
+	"encoding/pem"
+	"fmt"
+	"golang.org/x/crypto/ssh"
+	"log"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/giolekva/pcloud/core/installer/soft"
+	"github.com/spf13/cobra"
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chart/loader"
+	"helm.sh/helm/v3/pkg/kube"
+)
+
+var bootstrapFlags struct {
+	chartsDir    string
+	adminPubKey  string
+	adminPrivKey string
+}
+
+func bootstrapCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "bootstrap",
+		RunE: bootstrapCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&bootstrapFlags.chartsDir,
+		"charts-dir",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&bootstrapFlags.adminPubKey,
+		"admin-pub-key",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&bootstrapFlags.adminPrivKey,
+		"admin-priv-key",
+		"",
+		"",
+	)
+	return cmd
+}
+
+func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
+	adminPubKey, adminPrivKey, err := readAdminKeys()
+	if err != nil {
+		return err
+	}
+	fluxPub, fluxPriv, err := generateSSHKeys()
+	if err != nil {
+		return err
+	}
+	softServePub, softServePriv, err := generateSSHKeys()
+	if err != nil {
+		return err
+	}
+	fmt.Println("Installing SoftServe")
+	if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
+		return err
+	}
+	time.Sleep(30 * time.Second)
+	ss, err := soft.NewClient("192.168.0.208", 22, adminPrivKey, log.Default())
+	if err != nil {
+		return err
+	}
+	if err := ss.UpdateConfig(
+		soft.DefaultConfig([]string{string(adminPubKey), fluxPub}),
+		"set admin keys"); err != nil {
+		return err
+	}
+	if err := ss.ReloadConfig(); err != nil {
+		return err
+	}
+	fmt.Println("Creating /pcloud repo")
+	if err := ss.AddRepository("pcloud", "# PCloud Systems\n"); err != nil {
+		return err
+	}
+	fmt.Println("Installing Flux")
+	if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
+		return err
+	}
+	return nil
+}
+
+func installSoftServe(pubKey, privKey, adminKey string) error {
+	config, err := createActionConfig()
+	if err != nil {
+		return err
+	}
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "soft-serve"))
+	if err != nil {
+		return err
+	}
+	values := map[string]interface{}{
+		"privateKey": privKey,
+		"publicKey":  pubKey,
+		"adminKey":   adminKey,
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "pcloud"
+	installer.CreateNamespace = true
+	installer.ReleaseName = "soft-serve"
+	installer.Wait = true
+	installer.Timeout = 5 * time.Minute
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
+		return err
+	}
+	return nil
+}
+
+func installFlux(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
+	config, err := createActionConfig()
+	if err != nil {
+		return err
+	}
+	chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "flux-bootstrap"))
+	if err != nil {
+		return err
+	}
+	values := map[string]interface{}{
+		"repositoryAddress":       repoAddr,
+		"repositoryHost":          repoHost,
+		"repositoryHostPublicKey": repoHostPubKey,
+		"privateKey":              privateKey,
+	}
+	installer := action.NewInstall(config)
+	installer.Namespace = "pcloud"
+	installer.CreateNamespace = true
+	installer.ReleaseName = "flux"
+	installer.Wait = true
+	installer.WaitForJobs = true
+	installer.Timeout = 5 * time.Minute
+	if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
+		return err
+	}
+	return nil
+}
+
+func createActionConfig() (*action.Configuration, error) {
+	config := new(action.Configuration)
+	if err := config.Init(
+		kube.GetConfig(rootFlags.kubeConfig, "", ""),
+		"pcloud",
+		"",
+		func(fmtString string, args ...interface{}) {
+			fmt.Printf(fmtString, args...)
+			fmt.Println()
+		},
+	); err != nil {
+		return nil, err
+	}
+	return config, nil
+}
+
+func generateSSHKeys() (string, string, error) {
+	pub, priv, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		return "", "", err
+	}
+	privEnc, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		return "", "", err
+	}
+	privPem := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "PRIVATE KEY",
+			Bytes: privEnc,
+		},
+	)
+	pubKey, err := ssh.NewPublicKey(pub)
+	if err != nil {
+		return "", "", err
+	}
+	return string(ssh.MarshalAuthorizedKey(pubKey)), string(privPem), nil
+}
+
+func readAdminKeys() ([]byte, []byte, error) {
+	pubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	privKey, err := os.ReadFile(bootstrapFlags.adminPrivKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	return pubKey, privKey, nil
+}
diff --git a/core/installer/cmd/env.go b/core/installer/cmd/env.go
new file mode 100644
index 0000000..6f24d19
--- /dev/null
+++ b/core/installer/cmd/env.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/giolekva/pcloud/core/installer/soft"
+	"github.com/spf13/cobra"
+)
+
+var createEnvFlags struct {
+	name         string
+	adminPrivKey string
+}
+
+func createEnvCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:  "create-env",
+		RunE: createEnvCmdRun,
+	}
+	cmd.Flags().StringVar(
+		&createEnvFlags.name,
+		"name",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
+		&createEnvFlags.adminPrivKey,
+		"admin-priv-key",
+		"",
+		"",
+	)
+	return cmd
+}
+
+func createEnvCmdRun(cmd *cobra.Command, args []string) error {
+	adminPrivKey, err := os.ReadFile(createEnvFlags.adminPrivKey)
+	if err != nil {
+		return err
+	}
+	ss, err := soft.NewClient("192.168.0.208", 22, adminPrivKey, log.Default())
+	if err != nil {
+		return err
+	}
+	readme := fmt.Sprintf("# %s PCloud environment", createEnvFlags.name)
+	if err := ss.AddRepository(createEnvFlags.name, readme); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/core/installer/cmd/main.go b/core/installer/cmd/main.go
new file mode 100644
index 0000000..9b7a6ba
--- /dev/null
+++ b/core/installer/cmd/main.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"log"
+
+	"github.com/spf13/cobra"
+)
+
+var rootCmd *cobra.Command
+
+var rootFlags struct {
+	kubeConfig string
+}
+
+func init() {
+	rootCmd = &cobra.Command{
+		Use: "pcloud",
+	}
+	rootCmd.PersistentFlags().StringVar(
+		&rootFlags.kubeConfig,
+		"kubeconfig",
+		"",
+		"",
+	)
+	rootCmd.AddCommand(bootstrapCmd())
+	rootCmd.AddCommand(createEnvCmd())
+	rootCmd.AddCommand(installCmd())
+}
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}