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
-}