diff --git a/cmd/helm/init.go b/cmd/helm/init.go index a258e128c..f665bbe42 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -19,14 +19,19 @@ package main import ( "fmt" "io" + "io/ioutil" "os" + "github.com/Masterminds/semver" + "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + "k8s.io/helm/pkg/plugin/installer" "k8s.io/helm/pkg/repo" ) @@ -42,10 +47,20 @@ const ( type initOptions struct { skipRefresh bool // --skip-refresh stableRepositoryURL string // --stable-repo-url + pluginsFilename string // --plugins home helmpath.Home } +type pluginsFileEntry struct { + URL string `json:"url"` + Version string `json:"version,omitempty"` +} + +type pluginsFile struct { + Plugins []*pluginsFileEntry `json:"plugins"` +} + func newInitCmd(out io.Writer) *cobra.Command { o := &initOptions{} @@ -63,6 +78,7 @@ func newInitCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache") f.StringVar(&o.stableRepositoryURL, "stable-repo-url", defaultStableRepositoryURL, "URL for stable repository") + f.StringVar(&o.pluginsFilename, "plugins", "", "a YAML file specifying plugins to install") return cmd } @@ -78,6 +94,11 @@ func (o *initOptions) run(out io.Writer) error { if err := ensureRepoFileFormat(o.home.RepositoryFile(), out); err != nil { return err } + if o.pluginsFilename != "" { + if err := ensurePluginsInstalled(o.pluginsFilename, out); err != nil { + return err + } + } fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", settings.Home) fmt.Fprintln(out, "Happy Helming!") return nil @@ -163,3 +184,71 @@ func ensureRepoFileFormat(file string, out io.Writer) error { } return nil } + +func ensurePluginsInstalled(pluginsFilename string, out io.Writer) error { + bytes, err := ioutil.ReadFile(pluginsFilename) + if err != nil { + return err + } + + pf := new(pluginsFile) + if err := yaml.Unmarshal(bytes, &pf); err != nil { + return errors.Wrapf(err, "failed to parse %s", pluginsFilename) + } + + for _, requiredPlugin := range pf.Plugins { + if err := ensurePluginInstalled(requiredPlugin, pluginsFilename, out); err != nil { + return errors.Wrapf(err, "failed to install plugin from %s", requiredPlugin.URL) + } + } + + return nil +} + +func ensurePluginInstalled(requiredPlugin *pluginsFileEntry, pluginsFilename string, out io.Writer) error { + i, err := installer.NewForSource(requiredPlugin.URL, requiredPlugin.Version, settings.Home) + if err != nil { + return err + } + + if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { + if err := installer.Install(i); err != nil { + return err + } + + p, err := plugin.LoadDir(i.Path()) + if err != nil { + return err + } + + if err := runHook(p, plugin.Install); err != nil { + return err + } + + fmt.Fprintf(out, "Installed plugin: %s\n", p.Metadata.Name) + } else if requiredPlugin.Version != "" { + p, err := plugin.LoadDir(i.Path()) + if err != nil { + return err + } + + if p.Metadata.Version != "" { + pluginVersion, err := semver.NewVersion(p.Metadata.Version) + if err != nil { + return err + } + + constraint, err := semver.NewConstraint(requiredPlugin.Version) + if err != nil { + return err + } + + if !constraint.Check(pluginVersion) { + fmt.Fprintf(out, "WARNING: Installed plugin '%s' is at version %s, while %s specifies %s\n", + p.Metadata.Name, p.Metadata.Version, pluginsFilename, requiredPlugin.Version) + } + } + } + + return nil +} diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index 781939800..9aaa9a27c 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -24,6 +24,8 @@ import ( "k8s.io/helm/pkg/helm/helmpath" ) +const testPluginsFile = "testdata/plugins.yaml" + func TestEnsureHome(t *testing.T) { hh := helmpath.Home(testTempDir(t)) @@ -41,6 +43,9 @@ func TestEnsureHome(t *testing.T) { if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil { t.Error(err) } + if err := ensurePluginsInstalled(testPluginsFile, b); err != nil { + t.Error(err) + } expectedDirs := []string{hh.String(), hh.Repository(), hh.Cache()} for _, dir := range expectedDirs { @@ -56,4 +61,12 @@ func TestEnsureHome(t *testing.T) { } else if fi.IsDir() { t.Errorf("%s should not be a directory", fi) } + + if plugins, err := findPlugins(settings.PluginDirs()); err != nil { + t.Error(err) + } else if len(plugins) != 1 { + t.Errorf("Expected 1 plugin, got %d", len(plugins)) + } else if plugins[0].Metadata.Name != "testplugin" { + t.Errorf("Expected %s to be installed", "testplugin") + } } diff --git a/cmd/helm/testdata/plugins.yaml b/cmd/helm/testdata/plugins.yaml new file mode 100644 index 000000000..69086973e --- /dev/null +++ b/cmd/helm/testdata/plugins.yaml @@ -0,0 +1,3 @@ +plugins: +- name: testplugin + url: testdata/testplugin diff --git a/cmd/helm/testdata/testplugin/plugin.yaml b/cmd/helm/testdata/testplugin/plugin.yaml new file mode 100644 index 000000000..890292cbf --- /dev/null +++ b/cmd/helm/testdata/testplugin/plugin.yaml @@ -0,0 +1,4 @@ +name: testplugin +usage: "echo test" +description: "This echos test" +command: "echo test" diff --git a/docs/plugins.md b/docs/plugins.md index 185257bb8..e72143565 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -37,10 +37,20 @@ Plugins are installed using the `$ helm plugin install ` command. You $ helm plugin install https://github.com/technosophos/helm-template ``` -If you have a plugin tar distribution, simply untar the plugin into the -`$(helm home)/plugins` directory. +If you have a plugin tar distribution, simply untar the plugin into the `$(helm home)/plugins` directory. You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` -You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` +Alternatively, a set of plugins can be installed during the `helm init` process by using the `--plugins ` flag, where `file.yaml` looks like this: + +``` +plugins: +- name: helm-template + url: https://github.com/technosophos/helm-template +- name: helm-diff + url: https://github.com/databus23/helm-diff + version: 2.11.0+3 +``` + +The `name` field only exists to allow you to easily identify plugins, and does not serve a functional purpose. If a plugin specified in the file is already installed, it maintains its current version. ## Building Plugins