diff --git a/cmd/plugin_install.go b/cmd/plugin_install.go index 22180945e..ab093ee22 100644 --- a/cmd/plugin_install.go +++ b/cmd/plugin_install.go @@ -17,6 +17,7 @@ import ( func newPluginInstallCmd() *cobra.Command { var cloudURL string + var exact bool var file string var reinstall bool var cmd = &cobra.Command{ @@ -83,9 +84,18 @@ func newPluginInstallCmd() *cobra.Command { // Now for each kind, name, version pair, download it from the release website, and install it. for _, install := range installs { - // If the plugin already exists, don't download it unless --reinstall was passed. - if !reinstall && workspace.HasPlugin(install) { - continue + // If the plugin already exists, don't download it unless --reinstall was passed. Note that + // by default we accept plugins with >= constraints, unless --exact was passed which requires ==. + if !reinstall { + if exact { + if workspace.HasPlugin(install) { + continue + } + } else { + if has, _ := workspace.HasPluginGTE(install); has { + continue + } + } } // If we got here, actually try to do the download. @@ -115,6 +125,8 @@ func newPluginInstallCmd() *cobra.Command { cmd.PersistentFlags().StringVarP(&cloudURL, "cloud-url", "c", "", "A cloud URL to download releases from") + cmd.PersistentFlags().BoolVarP(&exact, + "exact", "e", false, "Force installation of an exact version match (usually >= is accepted)") cmd.PersistentFlags().StringVarP(&file, "file", "f", "", "Install a plugin from a tarball file, instead of downloading it") cmd.PersistentFlags().BoolVar(&reinstall, diff --git a/pkg/resource/plugin/host.go b/pkg/resource/plugin/host.go index 6e20a71fd..03b36baed 100644 --- a/pkg/resource/plugin/host.go +++ b/pkg/resource/plugin/host.go @@ -165,13 +165,12 @@ func (host *defaultHost) Provider(pkg tokens.Package, version *semver.Version) ( contract.Assert(plug != nil) // Make sure the versions match. - // TODO: support loading multiple plugin versions side-by-side. if version != nil { if plug.Info.Version == nil { return nil, errors.Errorf("resource plugin version %s requested, but an unknown version was found", version.String()) - } else if !version.EQ(*plug.Info.Version) { + } else if !plug.Info.Version.GTE(*version) { return nil, errors.Errorf("resource plugin version %s requested, but version %s was found", version.String(), plug.Info.Version.String()) @@ -191,13 +190,15 @@ func (host *defaultHost) Provider(pkg tokens.Package, version *semver.Version) ( // Warn if the plugin version was not what we expected if version != nil { - if info.Version == nil || !version.EQ(*info.Version) { + if info.Version == nil || !info.Version.GTE(*version) { var v string if info.Version != nil { v = info.Version.String() } host.ctx.Diag.Warningf( - diag.Message("resource plugin %s mis-reported its own version, expected %s got %s"), + diag.Message( + "resource plugin %s is expected to have version >=%s, but has %s; "+ + "the wrong version may be on your path, or this may be a bug in the plugin"), info.Name, version.String(), v) } } diff --git a/pkg/workspace/plugins.go b/pkg/workspace/plugins.go index 3184b0847..9b8d8fd2f 100644 --- a/pkg/workspace/plugins.go +++ b/pkg/workspace/plugins.go @@ -217,6 +217,28 @@ func HasPlugin(plug PluginInfo) bool { return false } +// HasPluginGTE returns true if the given plugin exists at the given version number or greater. +func HasPluginGTE(plug PluginInfo) (bool, error) { + // If an exact match, return true right away. + if HasPlugin(plug) { + return true, nil + } + + // Otherwise, load up the list of plugins and find one with the same name/type and >= version. + plugs, err := GetPlugins() + if err != nil { + return false, err + } + for _, p := range plugs { + if p.Name == plug.Name && + p.Kind == plug.Kind && + (p.Version != nil && plug.Version != nil && p.Version.GTE(*plug.Version)) { + return true, nil + } + } + return false, nil +} + // GetPluginDir returns the directory in which plugins on the current machine are managed. func GetPluginDir() (string, error) { u, err := user.Current() @@ -260,54 +282,57 @@ func GetPlugins() ([]PluginInfo, error) { return plugins, nil } -// GetPluginPath finds a plugin's path by its kind, name, and optional version. If no version is supplied, the latest -// plugin for that given kind/name pair is loaded, using standard semver sorting rules. +// GetPluginPath finds a plugin's path by its kind, name, and optional version. It will match the latest version that +// is >= the version specified. If no version is supplied, the latest plugin for that given kind/name pair is loaded, +// using standard semver sorting rules. A plugin may be overridden entirely by placing it on your $PATH. func GetPluginPath(kind PluginKind, name string, version *semver.Version) (string, string, error) { - // If we have a version, check the plugin cache first. - if version != nil { - plugins, err := GetPlugins() - if err != nil { - return "", "", errors.Wrapf(err, "loading plugin list") - } - var match *PluginInfo - for _, plugin := range plugins { - if plugin.Kind == kind && plugin.Name == name { - if version == nil { - // If no version filter was specified, pick the most recent version. But we must also keep going - // because we could later on find a version that is even more recent and should take precedence. - if match == nil || match.Version == nil || - (plugin.Version != nil && (*match).Version.LT(*plugin.Version)) { - match = &plugin - } - } else if plugin.Version != nil && (*version).EQ(*plugin.Version) { - // If there's a specific version being sought, and we found it, we're done. - match = &plugin - break - } - } - } + // If we have a version of the plugin on its $PATH, use it. This supports development scenarios. + filename := (&PluginInfo{Kind: kind, Name: name, Version: version}).FilePrefix() + if path, err := exec.LookPath(filename); err == nil { + glog.V(6).Infof("GetPluginPath(%s, %s, %v): found on $PATH %s", kind, name, version, path) + return "", path, nil + } - if match != nil { - matchDir, err := match.DirPath() - if err != nil { - return "", "", err - } - matchPath, err := match.FilePath() - if err != nil { - return "", "", err + // Otherwise, check the plugin cache. + plugins, err := GetPlugins() + if err != nil { + return "", "", errors.Wrapf(err, "loading plugin list") + } + var match *PluginInfo + for _, plugin := range plugins { + if plugin.Kind == kind && plugin.Name == name { + // Always pick the most recent version of the plugin available. Even if this is an exact match, we + // keep on searching just in case there's a newer version available. + var m *PluginInfo + if match == nil && version == nil { + m = &plugin // no existing match, no version spec, take it. + } else if match != nil && + (match.Version == nil || (plugin.Version != nil && plugin.Version.GT(*match.Version))) { + m = &plugin // existing match, but this plugin is newer, prefer it. + } else if version != nil && plugin.Version != nil && plugin.Version.GTE(*version) { + m = &plugin // this plugin is >= the version being requested, use it. } - glog.V(9).Infof("GetPluginPath(%s, %s, %v): found in cache at %s", kind, name, version, matchPath) - return matchDir, matchPath, nil + if m != nil { + match = m + glog.V(6).Infof("GetPluginPath(%s, %s, %s): found candidate (#%s)", + kind, name, *version, match.Version) + } } } - // If we don't have a version (or we do, but it wasn't in the cache), then fall back to the version on the $PATH. - // This supports development scenarios where we want to make it easy to override. - filename := (&PluginInfo{Kind: kind, Name: name, Version: version}).FilePrefix() - if path, err := exec.LookPath(filename); err == nil { - glog.V(9).Infof("GetPluginPath(%s, %s, %v): found on path %s", kind, name, version, path) - return "", path, nil + if match != nil { + matchDir, err := match.DirPath() + if err != nil { + return "", "", err + } + matchPath, err := match.FilePath() + if err != nil { + return "", "", err + } + + glog.V(6).Infof("GetPluginPath(%s, %s, %v): found in cache at %s", kind, name, version, matchPath) + return matchDir, matchPath, nil } return "", "", nil