appmanager: helm package unpacker
diff --git a/appmanager/cmd/main.go b/appmanager/cmd/main.go
new file mode 100644
index 0000000..ec7dcd6
--- /dev/null
+++ b/appmanager/cmd/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	app "github.com/giolekva/pcloud/appmanager"
+
+	"github.com/golang/glog"
+)
+
+func main() {
+	unpacker := app.NewHelmUnpacker("/usr/local/bin/helm")
+	temps, err := unpacker.Unpack("/Users/lekva/dev/go/src/github.com/giolekva/pcloud/apps/rpuppy/chart",
+		"app-rpuppy",
+		"init-release",
+		map[string]string{
+			"replicas":    "2",
+			"servicePort": "8080",
+		},
+	)
+	if err != nil {
+		panic(err)
+	}
+	glog.Info(temps)
+}
diff --git a/appmanager/go.mod b/appmanager/go.mod
new file mode 100644
index 0000000..966e59e
--- /dev/null
+++ b/appmanager/go.mod
@@ -0,0 +1,5 @@
+module github.com/giolekva/pcloud/appmanager
+
+go 1.14
+
+require github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
diff --git a/appmanager/go.sum b/appmanager/go.sum
new file mode 100644
index 0000000..e323037
--- /dev/null
+++ b/appmanager/go.sum
@@ -0,0 +1,2 @@
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
diff --git a/appmanager/unpacker.go b/appmanager/unpacker.go
new file mode 100644
index 0000000..e56d5f2
--- /dev/null
+++ b/appmanager/unpacker.go
@@ -0,0 +1,90 @@
+package appmanager
+
+import (
+	"errors"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/golang/glog"
+)
+
+type Unpacker interface {
+	Unpack(archive string,
+		namespace string,
+		releaseRame string,
+		values map[string]string) (map[string][]string, error)
+}
+
+type helmUnpacker struct {
+	helmBin string
+}
+
+func NewHelmUnpacker(helmBin string) Unpacker {
+	return &helmUnpacker{helmBin}
+}
+
+func (h *helmUnpacker) Unpack(
+	archive string,
+	namespace string,
+	releaseName string,
+	values map[string]string) (map[string][]string, error) {
+	cmd := h.generateHelmInstallCmd(archive, namespace, releaseName, values)
+	glog.Info(cmd.String())
+	var stdout strings.Builder
+	var stderr strings.Builder
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err := cmd.Run()
+	if err != nil {
+		return nil, errors.New(stderr.String())
+	}
+	return extractTemplates(stdout.String())
+}
+
+func (h *helmUnpacker) generateHelmInstallCmd(
+	archive string,
+	namespace string,
+	releaseName string,
+	values map[string]string) *exec.Cmd {
+	cmd := exec.Command(h.helmBin)
+	cmd.Args = append(cmd.Args, "template")
+	cmd.Args = append(cmd.Args, fmt.Sprintf("--namespace=%s", namespace))
+	cmd.Args = append(cmd.Args, fmt.Sprintf("%s", releaseName))
+	cmd.Args = append(cmd.Args, fmt.Sprintf("%s", archive))
+	// TODO(giolekva): validate values
+	for key, value := range values {
+		cmd.Args = append(cmd.Args, fmt.Sprintf("--set=%s=%s", key, value))
+	}
+	return cmd
+}
+
+func extractTemplates(bundle string) (map[string][]string, error) {
+	items := strings.Split(bundle, "---")
+	temps := make(map[string][]string)
+	for _, item := range items {
+		if len(item) == 0 {
+			continue
+		}
+		tmp := strings.SplitN(item, "\n", 3)
+		if len(tmp) != 3 {
+			return nil, fmt.Errorf("Got invalid template: %s", item)
+		}
+		source := tmp[1]
+		glog.Info(source)
+		// if !strings.HasPrefix(source, "\n# Source: ") {
+		// 	return nil, fmt.Errorf("Got invalid source: %s", item)
+		// }
+		sourceItems := strings.Split(source, "/")
+		glog.Info(sourceItems)
+		if len(sourceItems) != 3 {
+			return nil, fmt.Errorf("Got invalid source: %s", item)
+		}
+		path := sourceItems[1] + "/" + sourceItems[2]
+		if _, ok := temps[path]; !ok {
+			temps[path] = make([]string, 1)
+		}
+		temps[path] = append(temps[path], tmp[2])
+	}
+	return temps, nil
+}