From 577b00f4ed11f071711eaa0f1b0547d9b62fb5cc Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Mon, 7 Aug 2017 16:54:39 -0400 Subject: [PATCH] feat(*): add plugin update cmd --- cmd/draft/create_test.go | 2 +- cmd/draft/plugin.go | 1 + cmd/draft/plugin_remove_test.go | 2 +- cmd/draft/plugin_update.go | 99 ++++++++++++++++++++++ cmd/draft/plugin_update_test.go | 50 +++++++++++ pkg/plugin/installer/installer.go | 20 +++++ pkg/plugin/installer/local_installer.go | 6 ++ pkg/plugin/installer/vcs_installer.go | 30 +++++++ pkg/plugin/installer/vcs_installer_test.go | 63 ++++++++++++++ pkg/testing/{ => helpers}/helpers.go | 4 +- 10 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 cmd/draft/plugin_update.go create mode 100644 cmd/draft/plugin_update_test.go rename pkg/testing/{ => helpers}/helpers.go (92%) diff --git a/cmd/draft/create_test.go b/cmd/draft/create_test.go index 7beca9d..0b0fee1 100644 --- a/cmd/draft/create_test.go +++ b/cmd/draft/create_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/Azure/draft/pkg/draft/draftpath" - helpers "github.com/Azure/draft/pkg/testing" + "github.com/Azure/draft/pkg/testing/helpers" ) const gitkeepfile = ".gitkeep" diff --git a/cmd/draft/plugin.go b/cmd/draft/plugin.go index b4625dd..57672e1 100644 --- a/cmd/draft/plugin.go +++ b/cmd/draft/plugin.go @@ -29,6 +29,7 @@ func newPluginCmd(out io.Writer) *cobra.Command { newPluginInstallCmd(out), newPluginListCmd(out), newPluginRemoveCmd(out), + newPluginUpdateCmd(out), ) return cmd } diff --git a/cmd/draft/plugin_remove_test.go b/cmd/draft/plugin_remove_test.go index 92ccce3..0f95234 100644 --- a/cmd/draft/plugin_remove_test.go +++ b/cmd/draft/plugin_remove_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/draft/pkg/draft/draftpath" - helpers "github.com/Azure/draft/pkg/testing" + "github.com/Azure/draft/pkg/testing/helpers" ) func TestPluginRemoveCmd(t *testing.T) { diff --git a/cmd/draft/plugin_update.go b/cmd/draft/plugin_update.go new file mode 100644 index 0000000..ec16e01 --- /dev/null +++ b/cmd/draft/plugin_update.go @@ -0,0 +1,99 @@ +package main + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "k8s.io/helm/pkg/plugin" + + "github.com/Azure/draft/pkg/draft/draftpath" + "github.com/Azure/draft/pkg/plugin/installer" +) + +type pluginUpdateCmd struct { + names []string + home draftpath.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 Draft 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 = draftpath.Home(homePath()) + return nil +} + +func (pcmd *pluginUpdateCmd) run() error { + installer.Debug = flagDebug + pluginsDir := pluginDirPath(pcmd.home) + debug("loading installed plugins from %s", pluginsDir) + plugins, err := findPlugins(pluginsDir) + 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 draftpath.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) +} diff --git a/cmd/draft/plugin_update_test.go b/cmd/draft/plugin_update_test.go new file mode 100644 index 0000000..72ae39b --- /dev/null +++ b/cmd/draft/plugin_update_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/Azure/draft/pkg/draft/draftpath" +) + +func TestPluginUpdateCmd(t *testing.T) { + // move this to e2e test suite soon + target, err := newTestPluginEnv("", "") + if err != nil { + t.Fatal(err) + } + old, err := setupTestPluginEnv(target) + if err != nil { + t.Fatal(err) + } + defer teardownTestPluginEnv(target, old) + + home := draftpath.Home(draftHome) + buf := bytes.NewBuffer(nil) + + update := &pluginUpdateCmd{ + names: []string{"server"}, + home: home, + out: buf, + } + + if err := update.run(); err == nil { + t.Errorf("expected plugin update to err but did not") + } + + install := &pluginInstallCmd{ + source: "https://github.com/michelleN/draft-server", + version: "0.1.0", + home: home, + out: buf, + } + + if err := install.run(); err != nil { + t.Fatalf("Erroring installing plugin") + } + + if err := update.run(); err != nil { + t.Errorf("Erroring updating plugin: %v", err) + } + +} diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index f1ebe5d..99c77a5 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -24,6 +24,8 @@ type Installer interface { Install() error // Path is the directory of the installed plugin. Path() string + // Update updates a plugin to $DRAFT_HOME. + Update() error } // Install installs a plugin to $DRAFT_HOME @@ -40,6 +42,24 @@ func Install(i Installer) error { return i.Install() } +// Update updates a plugin in $DRAFT_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() +} + +// FindSource determines the correct Installer for the given source. +func FindSource(location string, home draftpath.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 +} + // New determines and returns the correct Installer for the given source func New(source, version string, home draftpath.Home) (Installer, error) { if isLocalReference(source) { diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index 127858c..595eca2 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -34,3 +34,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 2da61b1..ac5fb76 100644 --- a/pkg/plugin/installer/vcs_installer.go +++ b/pkg/plugin/installer/vcs_installer.go @@ -1,6 +1,7 @@ package installer import ( + "errors" "fmt" "os" "sort" @@ -31,6 +32,7 @@ func NewVCSInstaller(source, version string, home draftpath.Home) (*VCSInstaller if err != nil { return nil, err } + i := &VCSInstaller{ Repo: repo, Version: version, @@ -63,6 +65,34 @@ 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 i.Repo.IsDirty() { + return errors.New("plugin repo was modified") + } + if err := i.Repo.Update(); err != nil { + return err + } + if !isPlugin(i.Repo.LocalPath()) { + return ErrMissingMetadata + } + return nil +} + +func existingVCSRepo(location string, home draftpath.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 +} + // Filter a list of versions to only included semantic versions. The response // is a mapping of the original version to the semantic version. func getSemVers(refs []string) []*semver.Version { diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index 9cc2fcb..6726892 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -82,3 +82,66 @@ func TestVCSInstallerSuccess(t *testing.T) { t.Errorf("expected error for plugin exists, got (%v)", err) } } + +func TestVCSInstallerUpdate(t *testing.T) { + + dh, err := ioutil.TempDir("", "draft-home-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dh) + + home := draftpath.Home(dh) + if err := os.MkdirAll(home.Plugins(), 0755); err != nil { + t.Fatalf("Could not create %s: %s", home.Plugins(), err) + } + + source := "https://github.com/michelleN/draft-server" + i, err := New(source, "0.1.0", 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 modified, got none") + } else if err.Error() != "plugin repo was modified" { + t.Errorf("expected error for plugin modified, got (%v)", err) + } + +} diff --git a/pkg/testing/helpers.go b/pkg/testing/helpers/helpers.go similarity index 92% rename from pkg/testing/helpers.go rename to pkg/testing/helpers/helpers.go index dee85d2..877abe6 100644 --- a/pkg/testing/helpers.go +++ b/pkg/testing/helpers/helpers.go @@ -1,4 +1,4 @@ -package testing +package helpers import ( "io/ioutil" @@ -8,7 +8,7 @@ import ( "testing" ) -// copyTree copies src directory content tree to dest. +// CopyTree copies src directory content tree to dest. // If dest exists, it's deleted. // We don't handle symlinks (not needed in this test helper) func CopyTree(t *testing.T, src, dest string) {