mirror of https://github.com/hashicorp/terraform
The semver library we were using doesn't have support for a "pessimistic constraint" where e.g. the user wants to accept only minor or patch version upgrades. This is important for providers since users will generally want to pin their dependencies to not inadvertantly accept breaking changes. So here we switch to hashicorp's home-grown go-version library, which has the ~> constraint operator for this sort of constraint. Given how much the old version object was already intruding into the interface and creating dependency noise in callers, this also now wraps the "raw" go-version objects in package-local structs, thus keeping the details encapsulated and allowing callers to deal just with this package's own types.pull/15208/head
parent
a8a64c66c0
commit
a1e29ae290
@ -0,0 +1,26 @@
|
||||
package discovery
|
||||
|
||||
// PluginRequirements describes a set of plugins (assumed to be of a consistent
|
||||
// kind) that are required to exist and have versions within the given
|
||||
// corresponding sets.
|
||||
//
|
||||
// PluginRequirements is a map from plugin name to VersionSet.
|
||||
type PluginRequirements map[string]VersionSet
|
||||
|
||||
// Merge takes the contents of the receiver and the other given requirements
|
||||
// object and merges them together into a single requirements structure
|
||||
// that satisfies both sets of requirements.
|
||||
func (r PluginRequirements) Merge(other PluginRequirements) PluginRequirements {
|
||||
ret := make(PluginRequirements)
|
||||
for n, vs := range r {
|
||||
ret[n] = vs
|
||||
}
|
||||
for n, vs := range other {
|
||||
if existing, exists := ret[n]; exists {
|
||||
ret[n] = existing.Intersection(vs)
|
||||
} else {
|
||||
ret[n] = vs
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
version "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// A VersionStr is a string containing a possibly-invalid representation
|
||||
// of a semver version number. Call Parse on it to obtain a real Version
|
||||
// object, or discover that it is invalid.
|
||||
type VersionStr string
|
||||
|
||||
// Parse transforms a VersionStr into a Version if it is
|
||||
// syntactically valid. If it isn't then an error is returned instead.
|
||||
func (s VersionStr) Parse() (Version, error) {
|
||||
raw, err := version.NewVersion(string(s))
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
return Version{raw}, nil
|
||||
}
|
||||
|
||||
// Version represents a version number that has been parsed from
|
||||
// a semver string and known to be valid.
|
||||
type Version struct {
|
||||
// We wrap this here just because it avoids a proliferation of
|
||||
// direct go-version imports all over the place, and keeps the
|
||||
// version-processing details within this package.
|
||||
raw *version.Version
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return v.raw.String()
|
||||
}
|
||||
|
||||
func (v Version) newerThan(other Version) bool {
|
||||
return v.raw.GreaterThan(other.raw)
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
version "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// A ConstraintStr is a string containing a possibly-invalid representation
|
||||
// of a version constraint provided in configuration. Call Parse on it to
|
||||
// obtain a real Constraint object, or discover that it is invalid.
|
||||
type ConstraintStr string
|
||||
|
||||
// Parse transforms a ConstraintStr into a VersionSet if it is
|
||||
// syntactically valid. If it isn't then an error is returned instead.
|
||||
func (s ConstraintStr) Parse() (VersionSet, error) {
|
||||
raw, err := version.NewConstraint(string(s))
|
||||
if err != nil {
|
||||
return VersionSet{}, err
|
||||
}
|
||||
return VersionSet{raw}, nil
|
||||
}
|
||||
|
||||
// MustParse is like Parse but it panics if the constraint string is invalid.
|
||||
func (s ConstraintStr) MustParse() VersionSet {
|
||||
ret, err := s.Parse()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// VersionSet represents a set of versions which any given Version is either
|
||||
// a member of or not.
|
||||
type VersionSet struct {
|
||||
// Internally a version set is actually a list of constraints that
|
||||
// *remove* versions from the set. Thus a VersionSet with an empty
|
||||
// Constraints list would be one that contains *all* versions.
|
||||
raw version.Constraints
|
||||
}
|
||||
|
||||
// Has returns true if the given version is in the receiving set.
|
||||
func (s VersionSet) Has(v Version) bool {
|
||||
return s.raw.Check(v.raw)
|
||||
}
|
||||
|
||||
// Intersection combines the receving set with the given other set to produce a
|
||||
// set that is the intersection of both sets, which is to say that it contains
|
||||
// only the versions that are members of both sets.
|
||||
func (s VersionSet) Intersection(other VersionSet) VersionSet {
|
||||
raw := make(version.Constraints, 0, len(s.raw)+len(other.raw))
|
||||
|
||||
// Since "raw" is a list of constraints that remove versions from the set,
|
||||
// "Intersection" is implemented by concatenating together those lists,
|
||||
// thus leaving behind only the versions not removed by either list.
|
||||
raw = append(raw, s.raw...)
|
||||
raw = append(raw, other.raw...)
|
||||
|
||||
return VersionSet{raw}
|
||||
}
|
||||
|
||||
// String returns a string representation of the set members as a set
|
||||
// of range constraints.
|
||||
func (s VersionSet) String() string {
|
||||
return s.raw.String()
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersionSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
ConstraintStr string
|
||||
VersionStr string
|
||||
ShouldHave bool
|
||||
}{
|
||||
// These test cases are not exhaustive since the underlying go-version
|
||||
// library is well-tested. This is mainly here just to exercise our
|
||||
// wrapper code, but also used as an opportunity to cover some basic
|
||||
// but important cases such as the ~> constraint so that we'll be more
|
||||
// likely to catch any accidental breaking behavior changes in the
|
||||
// underlying library.
|
||||
{
|
||||
">=1.0.0",
|
||||
"1.0.0",
|
||||
true,
|
||||
},
|
||||
{
|
||||
">=1.0.0",
|
||||
"0.0.0",
|
||||
false,
|
||||
},
|
||||
{
|
||||
">=1.0.0",
|
||||
"1.1.0-beta1",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"~>1.1.0",
|
||||
"1.1.2-beta1",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"~>1.1.0",
|
||||
"1.2.0",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s has %s", test.ConstraintStr, test.VersionStr), func(t *testing.T) {
|
||||
accepted, err := ConstraintStr(test.ConstraintStr).Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("unwanted error parsing constraints string %q: %s", test.ConstraintStr, err)
|
||||
}
|
||||
|
||||
version, err := VersionStr(test.VersionStr).Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("unwanted error parsing version string %q: %s", test.VersionStr, err)
|
||||
}
|
||||
|
||||
if got, want := accepted.Has(version), test.ShouldHave; got != want {
|
||||
t.Errorf("Has returned %#v; want %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue