From 784a33962730b7884d0f6158d750d79f08cf705e Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 11 Nov 2016 11:12:57 -0700 Subject: [PATCH] feat(helm): support 'helm create --pack=mypack' This adds support for packs, pre-configured chart patterns that can be used to quickly create a custom layout for your new chart. --- cmd/helm/create.go | 15 ++++- cmd/helm/create_test.go | 92 ++++++++++++++++++++++++++++++ cmd/helm/helm_test.go | 2 +- cmd/helm/helmpath/helmhome.go | 5 ++ cmd/helm/helmpath/helmhome_test.go | 1 + cmd/helm/init.go | 2 +- docs/charts.md | 18 ++++++ pkg/chartutil/create.go | 11 ++++ pkg/chartutil/create_test.go | 51 +++++++++++++++++ pkg/chartutil/save.go | 55 ++++++++++++++++++ pkg/chartutil/save_test.go | 34 +++++++++++ 11 files changed, 282 insertions(+), 4 deletions(-) diff --git a/cmd/helm/create.go b/cmd/helm/create.go index a76e3299f..a4a09113e 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -54,8 +55,10 @@ will be overwritten, but other files will be left alone. ` type createCmd struct { - name string - out io.Writer + home helmpath.Home + name string + out io.Writer + starter string } func newCreateCmd(out io.Writer) *cobra.Command { @@ -68,6 +71,7 @@ func newCreateCmd(out io.Writer) *cobra.Command { Short: "create a new chart with the given name", Long: createDesc, RunE: func(cmd *cobra.Command, args []string) error { + cc.home = helmpath.Home(homePath()) if len(args) == 0 { return errors.New("the name of the new chart is required") } @@ -76,6 +80,7 @@ func newCreateCmd(out io.Writer) *cobra.Command { }, } + cmd.Flags().StringVarP(&cc.starter, "starter", "p", "", "the named Helm starter scaffold") return cmd } @@ -90,6 +95,12 @@ func (c *createCmd) run() error { ApiVersion: chartutil.ApiVersionV1, } + if c.starter != "" { + // Create from the starter + lstarter := filepath.Join(c.home.Starters(), c.starter) + return chartutil.CreateFrom(cfile, filepath.Dir(c.name), lstarter) + } + _, err := chartutil.Create(cfile, filepath.Dir(c.name)) return err } diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 5fb2c82a6..80e8dc74a 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -19,9 +19,11 @@ package main import ( "io/ioutil" "os" + "path/filepath" "testing" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" ) func TestCreateCmd(t *testing.T) { @@ -69,3 +71,93 @@ func TestCreateCmd(t *testing.T) { t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) } } + +func TestCreateStarterCmd(t *testing.T) { + cname := "testchart" + // Make a temp dir + tdir, err := ioutil.TempDir("", "helm-create-") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tdir) + + thome, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + old := homePath() + helmHome = thome + defer func() { + helmHome = old + os.RemoveAll(thome) + }() + + // Create a starter. + starterchart := filepath.Join(thome, "starters") + os.Mkdir(starterchart, 0755) + if dest, err := chartutil.Create(&chart.Metadata{Name: "starterchart"}, starterchart); err != nil { + t.Fatalf("Could not create chart: %s", err) + } else { + t.Logf("Created %s", dest) + } + tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") + if err := ioutil.WriteFile(tplpath, []byte("test"), 0755); err != nil { + t.Fatalf("Could not write template: %s", err) + } + + // CD into it + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(tdir); err != nil { + t.Fatal(err) + } + defer os.Chdir(pwd) + + // Run a create + cmd := newCreateCmd(os.Stdout) + cmd.ParseFlags([]string{"--starter", "starterchart"}) + if err := cmd.RunE(cmd, []string{cname}); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := chartutil.LoadDir(cname) + if err != nil { + t.Fatal(err) + } + + if c.Metadata.Name != cname { + t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + } + if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { + t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + } + + if l := len(c.Templates); l != 5 { + t.Errorf("Expected 5 templates, got %d", l) + } + + found := false + for _, tpl := range c.Templates { + if tpl.Name == "templates/foo.tpl" { + found = true + data := tpl.Data + if string(data) != "test" { + t.Errorf("Expected template 'test', got %q", string(data)) + } + } + } + if !found { + t.Error("Did not find foo.tpl") + } + +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 284d74b50..ac03e178a 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -241,7 +241,7 @@ func tempHelmHome(t *testing.T) (string, error) { // // t is used only for logging. func ensureTestHome(home helmpath.Home, t *testing.T) error { - configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()} + configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Starters()} for _, p := range configDirectories { if fi, err := os.Stat(p); err != nil { if err := os.MkdirAll(p, 0755); err != nil { diff --git a/cmd/helm/helmpath/helmhome.go b/cmd/helm/helmpath/helmhome.go index 798ab3d5f..9289c9d45 100644 --- a/cmd/helm/helmpath/helmhome.go +++ b/cmd/helm/helmpath/helmhome.go @@ -53,6 +53,11 @@ func (h Home) CacheIndex(name string) string { return filepath.Join(string(h), target) } +// Starters returns the path to the Helm starter packs. +func (h Home) Starters() string { + return filepath.Join(string(h), "starters") +} + // LocalRepository returns the location to the local repo. // // The local repo is the one used by 'helm serve' diff --git a/cmd/helm/helmpath/helmhome_test.go b/cmd/helm/helmpath/helmhome_test.go index d44bcd624..d7254f60d 100644 --- a/cmd/helm/helmpath/helmhome_test.go +++ b/cmd/helm/helmpath/helmhome_test.go @@ -33,4 +33,5 @@ func TestHelmHome(t *testing.T) { isEq(t, hh.LocalRepository(), "/r/repository/local") isEq(t, hh.Cache(), "/r/repository/cache") isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml") + isEq(t, hh.Starters(), "/r/starters") } diff --git a/cmd/helm/init.go b/cmd/helm/init.go index f5e248579..442b5bb92 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -143,7 +143,7 @@ func (i *initCmd) run() error { // // If $HELM_HOME does not exist, this function will create it. func ensureHome(home helmpath.Home, out io.Writer) error { - configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()} + configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Starters()} for _, p := range configDirectories { if fi, err := os.Stat(p); err != nil { fmt.Fprintf(out, "Creating %s \n", p) diff --git a/docs/charts.md b/docs/charts.md index 6955f0ab7..1e7b9a79f 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -543,3 +543,21 @@ commands. However, Helm does not provide tools for uploading charts to remote repository servers. This is because doing so would add substantial requirements to an implementing server, and thus raise the barrier for setting up a repository. + +## Chart Starter Packs + +The `helm create` command takes an optional `--starter` option that lets you +specify a "starter chart". + +Starters are just regular charts, but are located in `$HELM_HOME/starters`. +As a chart developer, you may author charts that are specifically designed +to be used as starters. Such charts should be designed with the following +considerations in mind: + +- The `Chart.yaml` will be overwritten by the genertor. +- Users will expect to modify such a chart's contents, so documentation + should indicate how users can do so. + +Currently the only way to add a chart to `$HELM_HOME/starters` is to manually +copy it there. In your chart's documentation, you may want to explain that +process. diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index 90d68c4f0..0fda5c2b9 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -175,6 +175,17 @@ We truncate at 24 chars because some Kubernetes name fields are limited to this {{- end -}} ` +// CreateFrom creates a new chart, but scaffolds it from the src chart. +func CreateFrom(chartfile *chart.Metadata, dest string, src string) error { + schart, err := Load(src) + if err != nil { + return fmt.Errorf("could not load %s: %s", src, err) + } + + schart.Metadata = chartfile + return SaveDir(schart, dest) +} + // Create creates a new chart in a directory. // // Inside of dir, this will create a directory based on the name of diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go index 19c2e041e..ee88972f4 100644 --- a/pkg/chartutil/create_test.go +++ b/pkg/chartutil/create_test.go @@ -75,3 +75,54 @@ func TestCreate(t *testing.T) { } } + +func TestCreateFrom(t *testing.T) { + tdir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + cf := &chart.Metadata{Name: "foo"} + srcdir := "./testdata/mariner" + + if err := CreateFrom(cf, tdir, srcdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + c := filepath.Join(tdir, cf.Name) + mychart, err := LoadDir(c) + if err != nil { + t.Fatalf("Failed to load newly created chart %q: %s", c, err) + } + + if mychart.Metadata.Name != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Metadata.Name) + } + + for _, d := range []string{TemplatesDir, ChartsDir} { + if fi, err := os.Stat(filepath.Join(dir, d)); err != nil { + t.Errorf("Expected %s dir: %s", d, err) + } else if !fi.IsDir() { + t.Errorf("Expected %s to be a directory.", d) + } + } + + for _, f := range []string{ChartfileName, ValuesfileName, "requirements.yaml"} { + if fi, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } else if fi.IsDir() { + t.Errorf("Expected %s to be a file.", f) + } + } + + for _, f := range []string{"placeholder.tpl"} { + if fi, err := os.Stat(filepath.Join(dir, TemplatesDir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } else if fi.IsDir() { + t.Errorf("Expected %s to be a file.", f) + } + } +} diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index 7ddb5e55e..e3b5c1afd 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -21,6 +21,7 @@ import ( "compress/gzip" "errors" "fmt" + "io/ioutil" "os" "path/filepath" @@ -31,6 +32,60 @@ import ( var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") +// SaveDir saves a chart as files in a directory. +func SaveDir(c *chart.Chart, dest string) error { + // Create the chart directory + outdir := filepath.Join(dest, c.Metadata.Name) + if err := os.Mkdir(outdir, 0755); err != nil { + return err + } + + // Save the chart file. + if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil { + return err + } + + // Save values.yaml + if c.Values != nil && len(c.Values.Raw) > 0 { + vf := filepath.Join(outdir, ValuesfileName) + if err := ioutil.WriteFile(vf, []byte(c.Values.Raw), 0755); err != nil { + return err + } + } + + for _, d := range []string{TemplatesDir, ChartsDir} { + if err := os.MkdirAll(filepath.Join(outdir, d), 0755); err != nil { + return err + } + } + + // Save templates + for _, f := range c.Templates { + n := filepath.Join(outdir, f.Name) + if err := ioutil.WriteFile(n, f.Data, 0755); err != nil { + return err + } + } + + // Save files + for _, f := range c.Files { + n := filepath.Join(outdir, f.TypeUrl) + if err := ioutil.WriteFile(n, f.Value, 0755); err != nil { + return err + } + } + + // Save dependencies + base := filepath.Join(outdir, ChartsDir) + for _, dep := range c.Dependencies { + // Here, we write each dependency as a tar file. + if _, err := Save(dep, base); err != nil { + return err + } + } + return nil +} + // Save creates an archived chart to the given directory. // // This takes an existing chart and a destination directory. diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index 28cc34dac..566999ff7 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -65,3 +65,37 @@ func TestSave(t *testing.T) { t.Fatal("Values data did not match") } } + +func TestSaveDir(t *testing.T) { + tmp, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "ahab", + Version: "1.2.3.4", + }, + Values: &chart.Config{ + Raw: "ship: Pequod", + }, + } + + if err := SaveDir(c, tmp); err != nil { + t.Fatalf("Failed to save: %s", err) + } + + c2, err := LoadDir(tmp + "/ahab") + if err != nil { + t.Fatal(err) + } + + if c2.Metadata.Name != c.Metadata.Name { + t.Fatalf("Expected chart archive to have %q, got %q", c.Metadata.Name, c2.Metadata.Name) + } + if c2.Values.Raw != c.Values.Raw { + t.Fatal("Values data did not match") + } +}