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