Permit plugin versions to float (#1122)

This change lets plugin versions to float in two ways:

1) If a `pulumi plugin install` detects a newer version is available
   already, there's no need to download and install the older version.

2) If the engine attempts to load a plugin at a particular version,
   if a newer version is available, it will be accepted without error.

As part of this, we permit $PATH to have the final say when determining
which version to accept.  That is, it can always override the choice.

Note that I highly suspect, in the limit, that we'll want to stop doing
this for major version incompatibilities. For now, since we don't
envision any such version changes imminently, this will suffice.
This commit is contained in:
Joe Duffy 2018-04-05 16:37:50 -07:00 committed by GitHub
parent 68d6df47b9
commit f2ae3a7afc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 86 additions and 48 deletions

View file

@ -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,

View file

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

View file

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