appmanager: with web interface to upload chart archives
diff --git a/appmanager/Dockerfile b/appmanager/Dockerfile
new file mode 100644
index 0000000..61c3f0d
--- /dev/null
+++ b/appmanager/Dockerfile
@@ -0,0 +1,23 @@
+FROM golang:1-alpine AS build
+
+RUN apk update && apk upgrade && \
+    apk add --no-cache bash git openssh
+
+WORKDIR /helm
+RUN wget https://get.helm.sh/helm-v3.2.1-linux-arm64.tar.gz
+RUN tar -xvf helm-v3.2.1-linux-arm64.tar.gz
+
+ENV GOOS linux
+ENV GOARCH $BUILDPLATFORM
+ENV CGO_ENABLED 0
+ENV GO111MODULE on
+
+WORKDIR $GOPATH/src/github.com/giolekva/pcloud/events
+COPY . .
+RUN go build -o $GOPATH/bin/app-manager -trimpath -ldflags="-s -w" cmd/main.go
+
+FROM alpine:latest
+COPY --from=build /go/bin/app-manager /usr/bin
+RUN chmod a+x /usr/bin/app-manager
+COPY --from=build /helm/linux-arm64/helm /usr/bin/helm
+RUN chmod a+x /usr/bin/helm
diff --git a/appmanager/cmd/main.go b/appmanager/cmd/main.go
index f3b009d..79658a1 100644
--- a/appmanager/cmd/main.go
+++ b/appmanager/cmd/main.go
@@ -1,22 +1,87 @@
 package main
 
 import (
-	app "github.com/giolekva/pcloud/appmanager"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
 
-	"github.com/golang/glog"
+	// "github.com/golang/glog"
+
+	app "github.com/giolekva/pcloud/appmanager"
 )
 
-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",
-		map[string]string{
-			"replicas":    "2",
-			"servicePort": "8080",
-		},
-	)
-	if err != nil {
-		panic(err)
+var port = flag.Int("port", 1234, "Port to listen on.")
+
+var helmUploadPage = `
+<html>
+<head>
+       <title>Upload Helm chart</title>
+</head>
+<body>
+<form enctype="multipart/form-data" action="/" method="post">
+    <input type="file" name="chartfile" />
+    <input type="submit" value="upload" />
+</form>
+</body>
+</html>
+`
+
+func helmHandler(w http.ResponseWriter, r *http.Request) {
+	if r.Method == "GET" {
+		_, err := io.WriteString(w, helmUploadPage)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	} else if r.Method == "POST" {
+		r.ParseMultipartForm(1000000)
+		file, handler, err := r.FormFile("chartfile")
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		defer file.Close()
+		p := "/tmp/" + handler.Filename
+		f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE, 0666)
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+		defer f.Close()
+		_, err = io.Copy(f, file)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if err = installHelmChart(p); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Write([]byte("Installed"))
 	}
-	glog.Info(temps)
+}
+
+func installHelmChart(path string) error {
+	h, err := app.HelmChartFromDir("/Users/lekva/dev/go/src/github.com/giolekva/pcloud/apps/rpuppy/chart")
+	if err != nil {
+		return err
+	}
+	// err = app.InstallSchema(h.Schema, "http://localhost:1111/add_schema")
+	// if err != nil {
+	// 	return err
+	// }
+	// glog.Infof("Installed schema: %s", h.Schema)
+	err = h.Install(
+		"/usr/local/bin/helm",
+		map[string]string{})
+	return err
+}
+
+func main() {
+	flag.Parse()
+	http.HandleFunc("/", helmHandler)
+	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+
 }
diff --git a/appmanager/go.mod b/appmanager/go.mod
index 966e59e..6e4e2e5 100644
--- a/appmanager/go.mod
+++ b/appmanager/go.mod
@@ -2,4 +2,8 @@
 
 go 1.14
 
-require github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+require (
+	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+	gopkg.in/yaml.v2 v2.2.8
+	gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 // indirect
+)
diff --git a/appmanager/go.sum b/appmanager/go.sum
index e323037..20097b2 100644
--- a/appmanager/go.sum
+++ b/appmanager/go.sum
@@ -1,2 +1,7 @@
 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=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/appmanager/helm.go b/appmanager/helm.go
new file mode 100644
index 0000000..5fd7e8e
--- /dev/null
+++ b/appmanager/helm.go
@@ -0,0 +1,113 @@
+package appmanager
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/golang/glog"
+	"gopkg.in/yaml.v2"
+)
+
+type Chart struct {
+	Name string `yaml:"name"`
+}
+
+type HelmChart struct {
+	Chart
+	chartDir string
+	Schema   *Schema
+	Yamls    []string
+}
+
+func HelmChartFromDir(chartDir string) (*HelmChart, error) {
+	var chart HelmChart
+	chart.chartDir = chartDir
+	c, err := ReadChart(path.Join(chartDir, "Chart.yaml"))
+	if err != nil {
+		return nil, err
+	}
+	chart.Chart = *c
+	schema, err := ReadSchema(path.Join(chartDir, "Schema.yaml"))
+	if err != nil && os.IsNotExist(err) {
+		return nil, err
+	}
+	chart.Schema = schema
+	return &chart, nil
+}
+
+func HelmChartFromTar(chartTar string) (*HelmChart, error) {
+	if !strings.HasSuffix(chartTar, ".tar.gz") {
+		return nil, errors.New("Expected .tar.gz file")
+	}
+	dir := filepath.Dir(chartTar)
+	archive := filepath.Base(chartTar)
+	extractDir := strings.TrimSuffix(archive, ".tar.gz")
+	cmd := exec.Command(fmt.Sprintf("cd %s && rm -rf %s && tar -ztvf %s -C %s", dir, extractDir, archive, extractDir))
+	if err := cmd.Run(); err != nil {
+		return nil, err
+	}
+	return HelmChartFromDir(dir + "/" + extractDir)
+}
+
+func (h *HelmChart) Install(
+	helmBin string,
+	values map[string]string) error {
+	namespace := fmt.Sprintf("app-%s", h.Chart.Name)
+	cmd := generateHelmInstallCmd(helmBin, h.chartDir, namespace, 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 errors.New(stderr.String())
+	}
+	glog.Info(stdout.String())
+	return nil
+}
+
+func generateHelmInstallCmd(
+	helmBin string,
+	archive string,
+	namespace string,
+	values map[string]string) *exec.Cmd {
+	cmd := exec.Command(helmBin)
+	cmd.Args = append(cmd.Args, "install")
+	cmd.Args = append(cmd.Args, fmt.Sprintf("--namespace=%s", namespace))
+	cmd.Args = append(cmd.Args, "--generate-name")
+	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 ChartFromYaml(str string) (*Chart, error) {
+	var s Chart
+	err := yaml.Unmarshal([]byte(str), &s)
+	if err != nil {
+		return nil, err
+	}
+	return &s, nil
+}
+
+func ReadChart(chartFile string) (*Chart, error) {
+	f, err := os.Open(chartFile)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	b, err := ioutil.ReadAll(f)
+	if err != nil {
+		return nil, err
+	}
+	return ChartFromYaml(string(b))
+}
diff --git a/appmanager/installer.go b/appmanager/installer.go
new file mode 100644
index 0000000..1fc5ae8
--- /dev/null
+++ b/appmanager/installer.go
@@ -0,0 +1,21 @@
+package appmanager
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+)
+
+func InstallSchema(schema *Schema, apiAddr string) error {
+	if schema == nil || len(schema.Schema) == 0 {
+		return nil
+	}
+	resp, err := http.Post(apiAddr, "application/text", strings.NewReader(schema.Schema))
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("Failed request with status code: %d", resp.StatusCode)
+	}
+	return nil
+}
diff --git a/appmanager/schema.go b/appmanager/schema.go
new file mode 100644
index 0000000..d1da046
--- /dev/null
+++ b/appmanager/schema.go
@@ -0,0 +1,34 @@
+package appmanager
+
+import (
+	"io/ioutil"
+	"os"
+
+	"gopkg.in/yaml.v2"
+)
+
+type Schema struct {
+	Schema string `yaml:"schema"`
+}
+
+func SchemaFromYaml(str string) (*Schema, error) {
+	var s Schema
+	err := yaml.Unmarshal([]byte(str), &s)
+	if err != nil {
+		return nil, err
+	}
+	return &s, nil
+}
+
+func ReadSchema(schemaFile string) (*Schema, error) {
+	f, err := os.Open(schemaFile)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	b, err := ioutil.ReadAll(f)
+	if err != nil {
+		return nil, err
+	}
+	return SchemaFromYaml(string(b))
+}
diff --git a/appmanager/unpacker.go b/appmanager/unpacker.go
deleted file mode 100644
index cd39985..0000000
--- a/appmanager/unpacker.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package appmanager
-
-import (
-	"errors"
-	"fmt"
-	"os/exec"
-	"strings"
-
-	"github.com/golang/glog"
-)
-
-type Unpacker interface {
-	Unpack(archive string,
-		namespace 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,
-	values map[string]string) (map[string][]string, error) {
-	cmd := h.generateHelmInstallCmd(archive, namespace, 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,
-	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, "--generate-name")
-	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
-}