From c349bfbffd60c498939c811516715e87c6ee2b0b Mon Sep 17 00:00:00 2001
From: Brian <brian@deis.com>
Date: Thu, 28 Apr 2016 17:24:08 -0600
Subject: [PATCH] feat(chart2proto): chart to proto transformations for helm
 grpc client

---
 cmd/helm/install.go  |  85 ++++++++++++++++++++----------
 pkg/chart/chart.go   |  43 ++++++++++++++++
 pkg/chart/values.go  |   5 ++
 pkg/helm/convert.go  | 120 +++++++++++++++++++++++++++++++++++++++++++
 pkg/helm/error.go    |  10 ++--
 pkg/helm/helm.go     |  89 ++++----------------------------
 pkg/helm/traverse.go | 110 +++++++++++++++++++++++++++++++++++++++
 7 files changed, 352 insertions(+), 110 deletions(-)
 create mode 100644 pkg/helm/convert.go
 create mode 100644 pkg/helm/traverse.go

diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index 8b515522c..ec1614692 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -3,22 +3,32 @@ package main
 import (
 	"fmt"
 	"os"
-	"path/filepath"
 
 	"github.com/spf13/cobra"
 
-	"github.com/kubernetes/helm/pkg/chart"
 	"github.com/kubernetes/helm/pkg/helm"
+	"github.com/kubernetes/helm/pkg/proto/hapi/release"
 )
 
 const installDesc = `
 This command installs a chart archive.
+
+The install argument must be either a relative
+path to a chart directory or the name of a
+chart in the current working directory.
 `
 
-func init() {
-	RootCommand.Flags()
-	RootCommand.AddCommand(installCmd)
-}
+const (
+	hostEnvVar  = "TILLER_HOST"
+	defaultHost = ":44134"
+)
+
+// install flags & args
+var (
+	installArg string // name or relative path of the chart to install
+	tillerHost string // override TILLER_HOST envVar
+	verbose    bool   // enable verbose install
+)
 
 var installCmd = &cobra.Command{
 	Use:   "install [CHART]",
@@ -28,38 +38,59 @@ var installCmd = &cobra.Command{
 }
 
 func runInstall(cmd *cobra.Command, args []string) error {
-	if len(args) == 0 {
-		return fmt.Errorf("This command needs at least one argument, the name of the chart.")
-	}
-
-	ch, err := loadChart(args[0])
-	if err != nil {
-		return err
-	}
+	setupInstallEnv(args)
 
-	res, err := helm.InstallRelease(ch)
+	res, err := helm.InstallRelease(installArg)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("release.name:   %s\n", res.Release.Name)
-	fmt.Printf("release.chart:  %s\n", res.Release.Chart.Metadata.Name)
-	fmt.Printf("release.status: %s\n", res.Release.Info.Status.Code)
+	printRelease(res.GetRelease())
 
 	return nil
 }
 
-func loadChart(path string) (*chart.Chart, error) {
-	path, err := filepath.Abs(path)
-	if err != nil {
-		return nil, err
+// TODO -- Display formatted description of install release status / info.
+// 		   Might be friendly to wrap our proto model with pretty-printers.
+//
+func printRelease(rel *release.Release) {
+	if verbose {
+		if rel != nil {
+			fmt.Printf("release.name:   %s\n", rel.Name)
+			fmt.Printf("release.info:   %s\n", rel.GetInfo())
+			fmt.Printf("release.chart:  %s\n", rel.GetChart())
+		}
+	}
+}
+
+func setupInstallEnv(args []string) {
+	if len(args) > 0 {
+		installArg = args[0]
+	} else {
+		fatalf("This command needs at least one argument, the name of the chart.")
 	}
 
-	if fi, err := os.Stat(path); err != nil {
-		return nil, err
-	} else if fi.IsDir() {
-		return chart.LoadDir(path)
+	// note: TILLER_HOST envvar is only
+	// acknowledged iff the host flag
+	// does not override the default.
+	if tillerHost == defaultHost {
+		host := os.Getenv(hostEnvVar)
+		if host != "" {
+			tillerHost = host
+		}
 	}
 
-	return chart.Load(path)
+	helm.Config.ServAddr = tillerHost
+}
+
+func fatalf(format string, args ...interface{}) {
+	fmt.Printf("fatal: %s\n", fmt.Sprintf(format, args...))
+	os.Exit(0)
+}
+
+func init() {
+	installCmd.Flags().StringVar(&tillerHost, "host", defaultHost, "address of tiller server")
+	installCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose install")
+
+	RootCommand.AddCommand(installCmd)
 }
diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go
index 73b948e26..b64823ced 100644
--- a/pkg/chart/chart.go
+++ b/pkg/chart/chart.go
@@ -98,6 +98,23 @@ func (c *Chart) LoadValues() (Values, error) {
 	return ReadValuesFile(filepath.Join(c.loader.dir(), preValues))
 }
 
+// ChartDepNames returns the list of chart names found in ChartsDir.
+func (c *Chart) ChartDepNames() ([]string, error) {
+	files, err := ioutil.ReadDir(c.ChartsDir())
+	if err != nil {
+		return nil, err
+	}
+
+	var deps []string
+	for _, file := range files {
+		if file.IsDir() {
+			deps = append(deps, filepath.Join(c.ChartsDir(), file.Name()))
+		}
+	}
+
+	return deps, nil
+}
+
 // chartLoader provides load, close, and save implementations for a chart.
 type chartLoader interface {
 	// Chartfile resturns a *Chartfile for this chart.
@@ -238,6 +255,32 @@ func LoadDir(chart string) (*Chart, error) {
 	}, nil
 }
 
+// LoadChart loads an entire chart archive.
+//
+// The following are valid values for 'chfi':
+//
+//		- relative path to the chart archive
+//		- absolute path to the chart archive
+// 		- name of the chart directory
+//
+func LoadChart(chfi string) (*Chart, error) {
+	path, err := filepath.Abs(chfi)
+	if err != nil {
+		return nil, err
+	}
+
+	fi, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+
+	if fi.IsDir() {
+		return LoadDir(path)
+	}
+
+	return Load(path)
+}
+
 // LoadData loads a chart from data, where data is a []byte containing a gzipped tar file.
 func LoadData(data []byte) (*Chart, error) {
 	return LoadDataFromReader(bytes.NewBuffer(data))
diff --git a/pkg/chart/values.go b/pkg/chart/values.go
index 20a712dc3..f86d2a368 100644
--- a/pkg/chart/values.go
+++ b/pkg/chart/values.go
@@ -2,6 +2,7 @@ package chart
 
 import (
 	"errors"
+	"io"
 	"io/ioutil"
 	"strings"
 
@@ -40,6 +41,10 @@ func (v Values) Table(name string) (Values, error) {
 	return table, err
 }
 
+func (v Values) Encode(w io.Writer) error {
+	return toml.NewEncoder(w).Encode(v)
+}
+
 func tableLookup(v Values, simple string) (Values, error) {
 	v2, ok := v[simple]
 	if !ok {
diff --git a/pkg/helm/convert.go b/pkg/helm/convert.go
new file mode 100644
index 000000000..f8e24e37c
--- /dev/null
+++ b/pkg/helm/convert.go
@@ -0,0 +1,120 @@
+package helm
+
+import (
+	"bytes"
+
+	chartutil "github.com/kubernetes/helm/pkg/chart"
+	chartpbs "github.com/kubernetes/helm/pkg/proto/hapi/chart"
+)
+
+func ChartToProto(ch *chartutil.Chart) (chpb *chartpbs.Chart, err error) {
+	chpb = new(chartpbs.Chart)
+
+	chpb.Metadata, err = MetadataToProto(ch)
+	if err != nil {
+		return
+	}
+
+	chpb.Templates, err = TemplatesToProto(ch)
+	if err != nil {
+		return
+	}
+
+	chpb.Values, err = ValuesToProto(ch)
+	if err != nil {
+		return
+	}
+
+	chs, err := WalkChartFile(ch)
+	if err != nil {
+		return
+	}
+
+	for _, dep := range chs.deps {
+		chdep, err := ChartToProto(dep.File())
+		if err != nil {
+			return nil, err
+		}
+
+		chpb.Dependencies = append(chpb.Dependencies, chdep)
+	}
+
+	return
+}
+
+func MetadataToProto(ch *chartutil.Chart) (*chartpbs.Metadata, error) {
+	if ch == nil {
+		return nil, ErrMissingChart
+	}
+
+	chfi := ch.Chartfile()
+
+	md := &chartpbs.Metadata{
+		Name:        chfi.Name,
+		Home:        chfi.Home,
+		Version:     chfi.Version,
+		Description: chfi.Description,
+	}
+
+	md.Sources = make([]string, len(chfi.Source))
+	copy(md.Sources, chfi.Source)
+
+	md.Keywords = make([]string, len(chfi.Keywords))
+	copy(md.Keywords, chfi.Keywords)
+
+	for _, maintainer := range chfi.Maintainers {
+		md.Maintainers = append(md.Maintainers, &chartpbs.Maintainer{
+			Name:  maintainer.Name,
+			Email: maintainer.Email,
+		})
+	}
+
+	return md, nil
+}
+
+func TemplatesToProto(ch *chartutil.Chart) (tpls []*chartpbs.Template, err error) {
+	if ch == nil {
+		return nil, ErrMissingChart
+	}
+
+	members, err := ch.LoadTemplates()
+	if err != nil {
+		return
+	}
+
+	var tpl *chartpbs.Template
+
+	for _, member := range members {
+		tpl = &chartpbs.Template{
+			Name: member.Path,
+			Data: make([]byte, len(member.Content)),
+		}
+
+		copy(tpl.Data, member.Content)
+
+		tpls = append(tpls, tpl)
+	}
+
+	return
+}
+
+func ValuesToProto(ch *chartutil.Chart) (*chartpbs.Config, error) {
+	if ch == nil {
+		return nil, ErrMissingChart
+	}
+
+	vals, err := ch.LoadValues()
+	if err != nil {
+		return nil, ErrMissingValues
+	}
+
+	var buf bytes.Buffer
+	if err = vals.Encode(&buf); err != nil {
+		return nil, err
+	}
+
+	cfgVals := new(chartpbs.Config)
+	cfgVals.Raw = buf.String()
+
+	return cfgVals, nil
+}
diff --git a/pkg/helm/error.go b/pkg/helm/error.go
index b8d72a2c4..781b670b8 100644
--- a/pkg/helm/error.go
+++ b/pkg/helm/error.go
@@ -1,11 +1,11 @@
 package helm
 
 const (
-	errNotImplemented = Error("helm api not implemented")
-	errMissingSrvAddr = Error("missing tiller address")
-	errMissingTpls    = Error("missing chart templates")
-	errMissingChart   = Error("missing chart metadata")
-	errMissingValues  = Error("missing chart values")
+	ErrNotImplemented = Error("helm api not implemented")
+	ErrInvalidSrvAddr = Error("invalid tiller address")
+	ErrMissingTpls    = Error("missing chart templates")
+	ErrMissingChart   = Error("missing chart metadata")
+	ErrMissingValues  = Error("missing chart values")
 )
 
 // Error represents a Helm client error.
diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go
index cf9993477..8a5913bb2 100644
--- a/pkg/helm/helm.go
+++ b/pkg/helm/helm.go
@@ -1,8 +1,7 @@
 package helm
 
 import (
-	"github.com/kubernetes/helm/pkg/chart"
-	chartpb "github.com/kubernetes/helm/pkg/proto/hapi/chart"
+	chartutil "github.com/kubernetes/helm/pkg/chart"
 	"github.com/kubernetes/helm/pkg/proto/hapi/services"
 	"golang.org/x/net/context"
 )
@@ -15,7 +14,7 @@ var Config = &config{
 
 // ListReleases lists the current releases.
 func ListReleases(limit, offset int) (<-chan *services.ListReleasesResponse, error) {
-	return nil, errNotImplemented
+	return nil, ErrNotImplemented
 }
 
 // GetReleaseStatus returns the given release's status.
@@ -45,7 +44,7 @@ func GetReleaseContent(name string) (*services.GetReleaseContentResponse, error)
 // UpdateRelease updates a release to a new/different chart.
 // TODO: This must take more than just name for an arg.
 func UpdateRelease(name string) (*services.UpdateReleaseResponse, error) {
-	return nil, errNotImplemented
+	return nil, ErrNotImplemented
 }
 
 // UninstallRelease uninstalls a named release and returns the response.
@@ -57,90 +56,24 @@ func UninstallRelease(name string) (*services.UninstallReleaseResponse, error) {
 }
 
 // InstallRelease installs a new chart and returns the release response.
-func InstallRelease(ch *chart.Chart) (res *services.InstallReleaseResponse, err error) {
-	chpb := new(chartpb.Chart)
-
-	chpb.Metadata, err = mkProtoMetadata(ch.Chartfile())
+func InstallRelease(chStr string) (*services.InstallReleaseResponse, error) {
+	chfi, err := chartutil.LoadChart(chStr)
 	if err != nil {
-		return
-	}
-
-	chpb.Templates, err = mkProtoTemplates(ch)
-	if err != nil {
-		return
+		return nil, err
 	}
 
-	chpb.Dependencies, err = mkProtoChartDeps(ch)
+	chpb, err := ChartToProto(chfi)
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	var vals *chartpb.Config
-
-	vals, err = mkProtoConfigValues(ch)
+	vals, err := ValuesToProto(chfi)
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	res, err = Config.client().install(&services.InstallReleaseRequest{
+	return Config.client().install(&services.InstallReleaseRequest{
 		Chart:  chpb,
 		Values: vals,
 	})
-
-	return
-}
-
-// pkg/chart to proto/hapi/chart helpers. temporary.
-func mkProtoMetadata(ch *chart.Chartfile) (*chartpb.Metadata, error) {
-	if ch == nil {
-		return nil, errMissingChart
-	}
-
-	md := &chartpb.Metadata{
-		Name:        ch.Name,
-		Home:        ch.Home,
-		Version:     ch.Version,
-		Description: ch.Description,
-	}
-
-	md.Sources = make([]string, len(ch.Source))
-	copy(md.Sources, ch.Source)
-
-	md.Keywords = make([]string, len(ch.Keywords))
-	copy(md.Keywords, ch.Keywords)
-
-	for _, maintainer := range ch.Maintainers {
-		md.Maintainers = append(md.Maintainers, &chartpb.Maintainer{
-			Name:  maintainer.Name,
-			Email: maintainer.Email,
-		})
-	}
-
-	return md, nil
-}
-
-func mkProtoTemplates(ch *chart.Chart) ([]*chartpb.Template, error) {
-	tpls, err := ch.LoadTemplates()
-	if err != nil {
-		return nil, err
-	}
-
-	_ = tpls
-
-	return nil, nil
-}
-
-func mkProtoChartDeps(ch *chart.Chart) ([]*chartpb.Chart, error) {
-	return nil, nil
-}
-
-func mkProtoConfigValues(ch *chart.Chart) (*chartpb.Config, error) {
-	vals, err := ch.LoadValues()
-	if err != nil {
-		return nil, errMissingValues
-	}
-
-	_ = vals
-
-	return nil, nil
 }
diff --git a/pkg/helm/traverse.go b/pkg/helm/traverse.go
new file mode 100644
index 000000000..4edcb409f
--- /dev/null
+++ b/pkg/helm/traverse.go
@@ -0,0 +1,110 @@
+package helm
+
+import (
+	chartutil "github.com/kubernetes/helm/pkg/chart"
+)
+
+//
+// TODO - we should probably consolidate
+// most of the code in this package, that
+// is specific to charts, into chartutil.
+//
+
+// Walk a chart's dependency tree, returning
+// a pointer to the root chart.
+//
+// The following is an example chart dependency
+// hierarchy and the structure of a chartObj
+// post traversal. (note some chart files are
+// omitted for brevity),
+//
+// mychart/
+//   charts/
+//      chart_A/
+//        charts/
+//           chart_B/
+//           chart_C/
+//             charts/
+//                chart_F/
+//       chart_D/
+//         charts/
+//            chart_E/
+//            chart_F/
+//
+//
+// chart: mychart (deps = 2)
+//      |
+//      |----> chart_A (deps = 2)
+//          |
+//          |--------> chart_B (deps = 0)
+//          |
+//          |--------> chart_C (deps = 1)
+//              |
+//              |------------> chart_F (deps = 0)
+//      |
+//      |----> chart_D (deps = 2)
+//          |
+//          |--------> chart_E (deps = 0)
+//          |
+//          |--------> chart_F (deps = 0)
+//
+//
+
+func WalkChartFile(chfi *chartutil.Chart) (*chartObj, error) {
+	root := &chartObj{file: chfi}
+	err := root.walkChartDeps(chfi)
+
+	return root, err
+}
+
+type chartObj struct {
+	file *chartutil.Chart
+	deps []*chartObj
+}
+
+func (chd *chartObj) File() *chartutil.Chart {
+	return chd.file
+}
+
+func (chs *chartObj) walkChartDeps(chfi *chartutil.Chart) error {
+	if hasDeps(chfi) {
+		names, err := chfi.ChartDepNames()
+		if err != nil {
+			return err
+		}
+
+		if len(names) > 0 {
+			chs.deps = append(chs.deps, resolveChartDeps(names)...)
+		}
+	}
+
+	return nil
+}
+
+func resolveChartDeps(names []string) (deps []*chartObj) {
+	for _, name := range names {
+		chfi, err := chartutil.LoadDir(name)
+		if err != nil {
+			return
+		}
+
+		chs := &chartObj{file: chfi}
+		err = chs.walkChartDeps(chfi)
+		if err != nil {
+			return
+		}
+
+		deps = append(deps, chs)
+	}
+
+	return
+}
+
+func hasDeps(chfi *chartutil.Chart) bool {
+	names, err := chfi.ChartDepNames()
+	if err != nil {
+		return false
+	}
+
+	return chfi.ChartsDir() != "" && len(names) > 0
+}
-- 
GitLab