diff --git a/cmd/helm/plugin.go b/cmd/helm/plugin.go index 37d9205b7..47e1b361c 100644 --- a/cmd/helm/plugin.go +++ b/cmd/helm/plugin.go @@ -41,6 +41,7 @@ func newPluginCmd(out io.Writer) *cobra.Command { newPluginInstallCmd(out), newPluginListCmd(out), newPluginRemoveCmd(out), + newPluginUpdateCmd(out), ) return cmd } diff --git a/cmd/helm/plugin_update.go b/cmd/helm/plugin_update.go new file mode 100644 index 000000000..e4fbe18df --- /dev/null +++ b/cmd/helm/plugin_update.go @@ -0,0 +1,114 @@ +/* +Copyright 2017 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strings" + + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin" + "k8s.io/helm/pkg/plugin/installer" + + "github.com/spf13/cobra" +) + +type pluginUpdateCmd struct { + names []string + home helmpath.Home + out io.Writer +} + +func newPluginUpdateCmd(out io.Writer) *cobra.Command { + pcmd := &pluginUpdateCmd{out: out} + cmd := &cobra.Command{ + Use: "update ...", + Short: "update one or more Helm plugins", + PreRunE: func(cmd *cobra.Command, args []string) error { + return pcmd.complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return pcmd.run() + }, + } + return cmd +} + +func (pcmd *pluginUpdateCmd) complete(args []string) error { + if len(args) == 0 { + return errors.New("please provide plugin name to update") + } + pcmd.names = args + pcmd.home = settings.Home + return nil +} + +func (pcmd *pluginUpdateCmd) run() error { + installer.Debug = settings.Debug + plugdirs := pluginDirs(pcmd.home) + debug("loading installed plugins from %s", plugdirs) + plugins, err := findPlugins(plugdirs) + if err != nil { + return err + } + var errorPlugins []string + + for _, name := range pcmd.names { + if found := findPlugin(plugins, name); found != nil { + if err := updatePlugin(found, pcmd.home); err != nil { + errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to update plugin %s, got error (%v)", name, err)) + } else { + fmt.Fprintf(pcmd.out, "Updated plugin: %s\n", name) + } + } else { + errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) + } + } + if len(errorPlugins) > 0 { + return fmt.Errorf(strings.Join(errorPlugins, "\n")) + } + return nil +} + +func updatePlugin(p *plugin.Plugin, home helmpath.Home) error { + exactLocation, err := filepath.EvalSymlinks(p.Dir) + if err != nil { + return err + } + absExactLocation, err := filepath.Abs(exactLocation) + if err != nil { + return err + } + + i, err := installer.FindSource(absExactLocation, home) + if err != nil { + return err + } + if err := installer.Update(i); err != nil { + return err + } + + debug("loading plugin from %s", i.Path()) + updatedPlugin, err := plugin.LoadDir(i.Path()) + if err != nil { + return err + } + + return runHook(updatedPlugin, plugin.Update, home) +} diff --git a/docs/helm/helm_plugin.md b/docs/helm/helm_plugin.md index 96d474dea..43267b973 100644 --- a/docs/helm/helm_plugin.md +++ b/docs/helm/helm_plugin.md @@ -24,5 +24,6 @@ Manage client-side Helm plugins. * [helm plugin install](helm_plugin_install.md) - install one or more Helm plugins * [helm plugin list](helm_plugin_list.md) - list installed Helm plugins * [helm plugin remove](helm_plugin_remove.md) - remove one or more Helm plugins +* [helm plugin update](helm_plugin_update.md) - update one or more Helm plugins -###### Auto generated by spf13/cobra on 16-Apr-2017 +###### Auto generated by spf13/cobra on 6-May-2017 diff --git a/docs/helm/helm_plugin_update.md b/docs/helm/helm_plugin_update.md new file mode 100644 index 000000000..f7c394618 --- /dev/null +++ b/docs/helm/helm_plugin_update.md @@ -0,0 +1,27 @@ +## helm plugin update + +update one or more Helm plugins + +### Synopsis + + +update one or more Helm plugins + +``` +helm plugin update ... +``` + +### Options inherited from parent commands + +``` + --debug enable verbose output + --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") + --host string address of tiller. Overrides $HELM_HOST + --kube-context string name of the kubeconfig context to use + --tiller-namespace string namespace of tiller (default "kube-system") +``` + +### SEE ALSO +* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins + +###### Auto generated by spf13/cobra on 6-May-2017 diff --git a/docs/man/man1/helm_plugin.1 b/docs/man/man1/helm_plugin.1 index 36bae074f..3ec070afd 100644 --- a/docs/man/man1/helm_plugin.1 +++ b/docs/man/man1/helm_plugin.1 @@ -1,4 +1,4 @@ -.TH "HELM" "1" "Apr 2017" "Auto generated by spf13/cobra" "" +.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" .nh .ad l @@ -42,9 +42,9 @@ Manage client\-side Helm plugins. .SH SEE ALSO .PP -\fBhelm(1)\fP, \fBhelm\-plugin\-install(1)\fP, \fBhelm\-plugin\-list(1)\fP, \fBhelm\-plugin\-remove(1)\fP +\fBhelm(1)\fP, \fBhelm\-plugin\-install(1)\fP, \fBhelm\-plugin\-list(1)\fP, \fBhelm\-plugin\-remove(1)\fP, \fBhelm\-plugin\-update(1)\fP .SH HISTORY .PP -16\-Apr\-2017 Auto generated by spf13/cobra +6\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin_update.1 b/docs/man/man1/helm_plugin_update.1 new file mode 100644 index 000000000..ef3d2270e --- /dev/null +++ b/docs/man/man1/helm_plugin_update.1 @@ -0,0 +1,50 @@ +.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" +.nh +.ad l + + +.SH NAME +.PP +helm\-plugin\-update \- update one or more Helm plugins + + +.SH SYNOPSIS +.PP +\fBhelm plugin update \&...\fP + + +.SH DESCRIPTION +.PP +update one or more Helm plugins + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-debug\fP[=false] + enable verbose output + +.PP +\fB\-\-home\fP="~/.helm" + location of your Helm config. Overrides $HELM\_HOME + +.PP +\fB\-\-host\fP="" + address of tiller. Overrides $HELM\_HOST + +.PP +\fB\-\-kube\-context\fP="" + name of the kubeconfig context to use + +.PP +\fB\-\-tiller\-namespace\fP="kube\-system" + namespace of tiller + + +.SH SEE ALSO +.PP +\fBhelm\-plugin(1)\fP + + +.SH HISTORY +.PP +6\-May\-2017 Auto generated by spf13/cobra diff --git a/pkg/plugin/hooks.go b/pkg/plugin/hooks.go index 1f435f9f8..b5ca032ac 100644 --- a/pkg/plugin/hooks.go +++ b/pkg/plugin/hooks.go @@ -21,6 +21,8 @@ const ( Install = "install" // Delete is executed after the plugin is removed. Delete = "delete" + // Update is executed after the plugin is updated. + Update = "update" ) // Hooks is a map of events to commands. diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 8da6e24e5..9b0c9a23b 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -36,6 +36,8 @@ type Installer interface { Install() error // Path is the directory of the installed plugin. Path() string + // Update updates a plugin to $HELM_HOME. + Update() error } // Install installs a plugin to $HELM_HOME. @@ -47,6 +49,15 @@ func Install(i Installer) error { return i.Install() } +// Update updates a plugin in $HELM_HOME. +func Update(i Installer) error { + if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { + return errors.New("plugin does not exist") + } + + return i.Update() +} + // NewForSource determines the correct Installer for the given source. func NewForSource(source, version string, home helmpath.Home) (Installer, error) { // Check if source is a local directory @@ -56,6 +67,15 @@ func NewForSource(source, version string, home helmpath.Home) (Installer, error) return NewVCSInstaller(source, version, home) } +// FindSource determines the correct Installer for the given source. +func FindSource(location string, home helmpath.Home) (Installer, error) { + installer, err := existingVCSRepo(location, home) + if err != nil && err.Error() == "Cannot detect VCS" { + return installer, errors.New("cannot get information about plugin source") + } + return installer, err +} + // isLocalReference checks if the source exists on the filesystem. func isLocalReference(source string) bool { _, err := os.Stat(source) diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index 7ab588d60..18011f8de 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -47,3 +47,9 @@ func (i *LocalInstaller) Install() error { } return i.link(src) } + +// Update updates a local repository +func (i *LocalInstaller) Update() error { + debug("local repository is auto-updated") + return nil +} diff --git a/pkg/plugin/installer/vcs_installer.go b/pkg/plugin/installer/vcs_installer.go index f9f181662..5285092a4 100644 --- a/pkg/plugin/installer/vcs_installer.go +++ b/pkg/plugin/installer/vcs_installer.go @@ -34,6 +34,18 @@ type VCSInstaller struct { base } +func existingVCSRepo(location string, home helmpath.Home) (Installer, error) { + repo, err := vcs.NewRepo("", location) + if err != nil { + return nil, err + } + i := &VCSInstaller{ + Repo: repo, + base: newBase(repo.Remote(), home), + } + return i, err +} + // NewVCSInstaller creates a new VCSInstaller. func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, error) { key, err := cache.Key(source) @@ -77,6 +89,18 @@ func (i *VCSInstaller) Install() error { return i.link(i.Repo.LocalPath()) } +// Update updates a remote repository +func (i *VCSInstaller) Update() error { + debug("updating %s", i.Repo.Remote()) + if err := i.Repo.Update(); err != nil { + return err + } + if !isPlugin(i.Repo.LocalPath()) { + return ErrMissingMetadata + } + return nil +} + func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) { if i.Version == "" { return "", nil diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index 081598c7c..cb346c661 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -97,6 +97,13 @@ func TestVCSInstaller(t *testing.T) { } else if err.Error() != "plugin already exists" { t.Errorf("expected error for plugin exists, got (%v)", err) } + + //Testing FindSource method, expect error because plugin code is not a cloned repository + if _, err := FindSource(i.Path(), home); err == nil { + t.Error("expected error for inability to find plugin source, got none") + } else if err.Error() != "cannot get information about plugin source" { + t.Errorf("expected error for inability to find plugin source, got (%v)", err) + } } func TestVCSInstallerNonExistentVersion(t *testing.T) { @@ -131,3 +138,66 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) { t.Errorf("expected error for version does not exists, got (%v)", err) } } +func TestVCSInstallerUpdate(t *testing.T) { + + hh, err := ioutil.TempDir("", "helm-home-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(hh) + + home := helmpath.Home(hh) + if err := os.MkdirAll(home.Plugins(), 0755); err != nil { + t.Fatalf("Could not create %s: %s", home.Plugins(), err) + } + + source := "https://github.com/adamreese/helm-env" + + i, err := NewForSource(source, "", home) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + // ensure a VCSInstaller was returned + _, ok := i.(*VCSInstaller) + if !ok { + t.Error("expected a VCSInstaller") + } + + if err := Update(i); err == nil { + t.Error("expected error for plugin does not exist, got none") + } else if err.Error() != "plugin does not exist" { + t.Errorf("expected error for plugin does not exist, got (%v)", err) + } + + // Install plugin before update + if err := Install(i); err != nil { + t.Error(err) + } + + // Test FindSource method for positive result + pluginInfo, err := FindSource(i.Path(), home) + if err != nil { + t.Error(err) + } + + repoRemote := pluginInfo.(*VCSInstaller).Repo.Remote() + if repoRemote != source { + t.Errorf("invalid source found, expected %q got %q", source, repoRemote) + } + + // Update plugin + if err := Update(i); err != nil { + t.Error(err) + } + + // Test update failure + os.Remove(filepath.Join(i.Path(), "plugin.yaml")) + // Testing update for error + if err := Update(i); err == nil { + t.Error("expected error for plugin metadata missing, got none") + } else if err.Error() != "plugin metadata (plugin.yaml) missing" { + t.Errorf("expected error for plugin metadata missing, got (%v)", err) + } + +} diff --git a/scripts/completions.bash b/scripts/completions.bash index be9ca298b..afe65c07b 100644 --- a/scripts/completions.bash +++ b/scripts/completions.bash @@ -1044,6 +1044,28 @@ _helm_plugin_remove() noun_aliases=() } +_helm_plugin_update() +{ + last_command="helm_plugin_update" + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--debug") + flags+=("--home=") + flags+=("--host=") + flags+=("--kube-context=") + flags+=("--tiller-namespace=") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _helm_plugin() { last_command="helm_plugin" @@ -1051,6 +1073,7 @@ _helm_plugin() commands+=("install") commands+=("list") commands+=("remove") + commands+=("update") flags=() two_word_flags=()