Merge pull request #1547 from technosophos/feat/packs

feat(helm): support 'helm create --starter=mypack'
pull/1576/head
Matt Butcher 8 years ago committed by GitHub
commit 5517d00a48

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
@ -54,8 +55,10 @@ will be overwritten, but other files will be left alone.
` `
type createCmd struct { type createCmd struct {
name string home helmpath.Home
out io.Writer name string
out io.Writer
starter string
} }
func newCreateCmd(out io.Writer) *cobra.Command { 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", Short: "create a new chart with the given name",
Long: createDesc, Long: createDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cc.home = helmpath.Home(homePath())
if len(args) == 0 { if len(args) == 0 {
return errors.New("the name of the new chart is required") 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 return cmd
} }
@ -90,6 +95,12 @@ func (c *createCmd) run() error {
ApiVersion: chartutil.ApiVersionV1, 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)) _, err := chartutil.Create(cfile, filepath.Dir(c.name))
return err return err
} }

@ -19,9 +19,11 @@ package main
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"testing" "testing"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
) )
func TestCreateCmd(t *testing.T) { func TestCreateCmd(t *testing.T) {
@ -69,3 +71,93 @@ func TestCreateCmd(t *testing.T) {
t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) 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")
}
}

@ -241,7 +241,7 @@ func tempHelmHome(t *testing.T) (string, error) {
// //
// t is used only for logging. // t is used only for logging.
func ensureTestHome(home helmpath.Home, t *testing.T) error { 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 { for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil { if fi, err := os.Stat(p); err != nil {
if err := os.MkdirAll(p, 0755); err != nil { if err := os.MkdirAll(p, 0755); err != nil {

@ -53,6 +53,11 @@ func (h Home) CacheIndex(name string) string {
return filepath.Join(string(h), target) 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. // LocalRepository returns the location to the local repo.
// //
// The local repo is the one used by 'helm serve' // The local repo is the one used by 'helm serve'

@ -33,4 +33,5 @@ func TestHelmHome(t *testing.T) {
isEq(t, hh.LocalRepository(), "/r/repository/local") isEq(t, hh.LocalRepository(), "/r/repository/local")
isEq(t, hh.Cache(), "/r/repository/cache") isEq(t, hh.Cache(), "/r/repository/cache")
isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml") isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml")
isEq(t, hh.Starters(), "/r/starters")
} }

@ -143,7 +143,7 @@ func (i *initCmd) run() error {
// //
// If $HELM_HOME does not exist, this function will create it. // If $HELM_HOME does not exist, this function will create it.
func ensureHome(home helmpath.Home, out io.Writer) error { 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 { for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil { if fi, err := os.Stat(p); err != nil {
fmt.Fprintf(out, "Creating %s \n", p) fmt.Fprintf(out, "Creating %s \n", p)

@ -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 remote repository servers. This is because doing so would add
substantial requirements to an implementing server, and thus raise the substantial requirements to an implementing server, and thus raise the
barrier for setting up a repository. 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.

@ -175,6 +175,17 @@ We truncate at 24 chars because some Kubernetes name fields are limited to this
{{- end -}} {{- 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. // Create creates a new chart in a directory.
// //
// Inside of dir, this will create a directory based on the name of // Inside of dir, this will create a directory based on the name of

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

@ -21,6 +21,7 @@ import (
"compress/gzip" "compress/gzip"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -31,6 +32,60 @@ import (
var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") 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. // Save creates an archived chart to the given directory.
// //
// This takes an existing chart and a destination directory. // This takes an existing chart and a destination directory.

@ -65,3 +65,37 @@ func TestSave(t *testing.T) {
t.Fatal("Values data did not match") 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")
}
}

Loading…
Cancel
Save