-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The Harvester Version library augments the widelyused SemVer package, adding Harvester-specific logic, especially for Harvester upgrades. It is meant to be consumed by the Harvester controller and command-line helper tool. Signed-off-by: Zespre Chang <[email protected]>
- Loading branch information
Showing
7 changed files
with
615 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package version | ||
|
||
import "errors" | ||
|
||
var ( | ||
ErrInvalidVersion = errors.New("invalid version string") | ||
ErrIncomparableVersion = errors.New("incomparable: dev version") | ||
|
||
ErrMinUpgradeRequirement = errors.New("current version does not meet minimum upgrade requirement") | ||
ErrDowngrade = errors.New("downgrading is prohibited") | ||
ErrDevUpgrade = errors.New("upgrading from dev versions to non-dev versions is prohibited") | ||
ErrPrereleaseCrossVersionUpgrade = errors.New("cross-version upgrades from/to any prerelease version are prohibited") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package version | ||
|
||
type HarvesterUpgradeVersion struct { | ||
currentVersion *HarvesterVersion | ||
upgradeVersion *HarvesterVersion | ||
minUpgradableVersion *HarvesterVersion | ||
} | ||
|
||
func NewHarvesterUpgradeVersion(cv, uv, mv *HarvesterVersion) *HarvesterUpgradeVersion { | ||
return &HarvesterUpgradeVersion{ | ||
currentVersion: cv, | ||
upgradeVersion: uv, | ||
minUpgradableVersion: mv, | ||
} | ||
} | ||
|
||
func (u *HarvesterUpgradeVersion) IsUpgrade() error { | ||
isDowngrade, err := u.currentVersion.IsNewer(u.upgradeVersion) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if isDowngrade { | ||
return ErrDowngrade | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// IsUpgradable checks that whether the current version satisfies the minimum upgrade requirement. | ||
func (u *HarvesterUpgradeVersion) IsUpgradable() error { | ||
// If minUpgradableVersion is nil means there's no restriction. | ||
if u.minUpgradableVersion == nil { | ||
return nil | ||
} | ||
|
||
isNotUpgradable, err := u.currentVersion.IsOlder(u.minUpgradableVersion) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if isNotUpgradable { | ||
return ErrMinUpgradeRequirement | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (u *HarvesterUpgradeVersion) CheckUpgradeEligibility(strictMode bool) error { | ||
// Upgrading to dev versions is always allowed | ||
if u.upgradeVersion.isDev { | ||
return nil | ||
} | ||
|
||
// Upgrading from dev versions is restricted if strict mode is enabled | ||
if u.currentVersion.isDev { | ||
if strictMode { | ||
return ErrDevUpgrade | ||
} | ||
return nil | ||
} | ||
|
||
// Same-version upgrade is always allowed | ||
isSameVersion, err := u.currentVersion.IsEqual(u.upgradeVersion) | ||
if err != nil { | ||
return err | ||
} | ||
if isSameVersion { | ||
return nil | ||
} | ||
|
||
// General cases | ||
// Check if it's effectively a downgrade | ||
if err := u.IsUpgrade(); err != nil { | ||
return err | ||
} | ||
|
||
// Check the minimum upgradable version | ||
if err := u.IsUpgradable(); err != nil { | ||
return err | ||
} | ||
|
||
// Check if it's a prerelease cross-version upgrade | ||
if u.currentVersion.isPrerelease { | ||
currentStableVersion := u.currentVersion.GetStableVersion() | ||
upgradeStableVersion := u.upgradeVersion.GetStableVersion() | ||
if isSameStableVersion, _ := currentStableVersion.IsEqual(upgradeStableVersion); !isSameStableVersion { | ||
return ErrPrereleaseCrossVersionUpgrade | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package version | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestHarvesterUpgradeVersion_IsUpgrade(t *testing.T) { | ||
var testCases = []struct { | ||
name string | ||
currentVersion string | ||
upgradeVersion string | ||
expectedErr error | ||
}{ | ||
{"upgrade from a newer release version to an older one", "v1.3.0", "v1.2.1", ErrDowngrade}, | ||
{"upgrade from an older release version to a newer one", "v1.2.2", "v1.3.0", nil}, | ||
|
||
{"upgrade from a newer prerelease version to an older one", "v1.3.0-rc1", "v1.2.1-rc1", ErrDowngrade}, | ||
{"upgrade from a newer prerelease version to an older one", "v1.2.1-rc2", "v1.2.1-rc1", ErrDowngrade}, | ||
{"upgrade from an older prerelease version to a newer one", "v1.2.1-rc1", "v1.3.0-rc1", nil}, | ||
{"upgrade from an older prerelease version to a newer one", "v1.2.1-rc1", "v1.2.1-rc2", nil}, | ||
|
||
{"upgrade among two dev versions", "11223344", "aabbccdd", ErrIncomparableVersion}, | ||
|
||
{"upgrade from a newer prerelease version to an older release version", "v1.2.2-rc2", "v1.2.1", ErrDowngrade}, | ||
{"upgrade from an older prerelease version to a newer release version", "v1.2.1-rc2", "v1.2.2", nil}, | ||
{"upgrade from a newer release version to an older prerelease version", "v1.2.2", "v1.2.2-rc2", ErrDowngrade}, | ||
{"upgrade from a newer release version to an older prerelease version", "v1.2.2", "v1.2.1-rc2", ErrDowngrade}, | ||
{"upgrade from an older release version to a newer prerelease version", "v1.2.1", "v1.2.2-rc1", nil}, | ||
|
||
{"upgrade from a release version to a dev version", "v1.2.1", "aabbccdd", ErrIncomparableVersion}, | ||
{"upgrade from a dev version to a release version", "aabbccdd", "v1.2.1", ErrIncomparableVersion}, | ||
{"upgrade from a prerelease version to a dev version", "v1.2.2-rc1", "aabbccdd", ErrIncomparableVersion}, | ||
{"upgrade from a dev version to a prerelease version", "aabbccdd", "v1.2.2-rc1", ErrIncomparableVersion}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
cv, _ := NewHarvesterVersion(tc.currentVersion) | ||
uv, _ := NewHarvesterVersion(tc.upgradeVersion) | ||
|
||
huv := NewHarvesterUpgradeVersion(cv, uv, nil) | ||
actualErr := huv.IsUpgrade() | ||
if tc.expectedErr != nil { | ||
assert.Equal(t, tc.expectedErr, actualErr, tc.name) | ||
} else { | ||
assert.Nil(t, actualErr, tc.name) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestHarvesterUpgradeVersion_IsUpgradable(t *testing.T) { | ||
var testCases = []struct { | ||
name string | ||
currentVersion string | ||
minUpgradableVersion string | ||
expectedErr error | ||
}{ | ||
{"upgrade from a release version above the minimal requirement", "v1.2.1", "v1.2.0", nil}, | ||
{"upgrade from a release version lower than the minimal requirement", "v1.2.1", "v1.2.2", ErrMinUpgradeRequirement}, | ||
{"upgrade from the exact same release version of the minimal requirement", "v1.2.1", "v1.2.1", nil}, | ||
|
||
{"upgrade from a prerelease version lower than the minimal requirement", "v1.2.1-rc1", "v1.2.1", ErrMinUpgradeRequirement}, | ||
{"upgrade from a prerelease version above the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc2", "v1.2.1-rc1", nil}, | ||
{"upgrade from a prerelease version lower than the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc1", "v1.2.1-rc2", ErrMinUpgradeRequirement}, | ||
{"upgrade from a prerelease version lower than the minimal requirement (rc minUpgradableVersion)", "v1.2.0-rc1", "v1.2.1-rc2", ErrMinUpgradeRequirement}, | ||
{"upgrade from the exact same prerelease version of the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc1", "v1.2.1-rc1", nil}, | ||
|
||
{"upgrade from dev versions", "11223344", "v1.2.1", ErrIncomparableVersion}, | ||
{"upgrade from dev versions", "aabbccdd", "v1.2.1", ErrIncomparableVersion}, | ||
|
||
{"upgrade from a release version without any minimum requirement specified", "v1.2.1", "", nil}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
cv, _ := NewHarvesterVersion(tc.currentVersion) | ||
mv, _ := NewHarvesterVersion(tc.minUpgradableVersion) | ||
|
||
huv := NewHarvesterUpgradeVersion(cv, nil, mv) | ||
actualErr := huv.IsUpgradable() | ||
if tc.expectedErr != nil { | ||
assert.Equal(t, tc.expectedErr, actualErr, tc.name) | ||
} else { | ||
assert.Nil(t, actualErr, tc.name) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestHarvesterUpgradeVersion_CheckUpgradeEligibility(t *testing.T) { | ||
var testCases = []struct { | ||
name string | ||
currentVersion string | ||
upgradeVersion string | ||
minUpgradableVersion string | ||
strictMode bool | ||
expectedErr error | ||
}{ | ||
{"upgrade to same release version", "v1.2.1", "v1.2.1", "v1.1.2", true, nil}, | ||
{"upgrade from an old release version same as the minimal requirement of a new release version", "v1.1.2", "v1.2.1", "v1.1.2", true, nil}, | ||
{"upgrade from an old release version above the minimal requirement of a new release version", "v1.2.0", "v1.2.1", "v1.1.2", true, nil}, | ||
{"upgrade from an old release version below the minimal requirement of a new release version", "v1.1.1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, | ||
{"upgrade from a new release version above the minimal requirement of an old release version", "v1.2.1", "v1.2.0", "v1.1.2", true, ErrDowngrade}, | ||
{"upgrade from an old release version same the minimal requirement of a new prerelease version", "v1.1.2", "v1.2.1-rc1", "v1.1.2", true, nil}, | ||
{"upgrade from an old release version above the minimal requirement of a new prerelease version", "v1.2.0", "v1.2.1-rc1", "v1.1.2", true, nil}, | ||
{"upgrade from an old release version below the minimal requirement of a new prerelease version", "v1.1.1", "v1.2.1-rc1", "v1.1.2", true, ErrMinUpgradeRequirement}, | ||
{"upgrade from a new release version above the minimal requirement of an old prerelease version", "v1.2.1", "v1.2.1-rc1", "v1.1.2", true, ErrDowngrade}, | ||
{"upgrade from a release version to a dev version", "v1.2.1", "v1.2-ab12cd34", "", true, nil}, | ||
|
||
{"upgrade to same prerelease version", "v1.2.2-rc1", "v1.2.2-rc1", "v1.2.1", true, nil}, | ||
{"upgrade from an old prerelease version below the minimal requirement of a new release version", "v1.1.2-rc1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, | ||
{"upgrade from an old prerelease version above the minimal requirement of a new release version", "v1.2.0-rc1", "v1.2.1", "v1.1.2", true, ErrPrereleaseCrossVersionUpgrade}, | ||
{"upgrade from an old prerelease version below the minimal requirement of a new release version", "v1.1.1-rc1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, | ||
{"upgrade from an old prerelease version above the minimal requirement of a new prerelease version", "v1.2.2-rc1", "v1.2.2-rc2", "v1.2.1", true, nil}, | ||
{"upgrade from an old prerelease version below the minimal requirement of a new prerelease version", "v1.2.1-rc1", "v1.2.2-rc2", "v1.2.1", true, ErrMinUpgradeRequirement}, | ||
{"upgrade from an old prerelease version above the minimal requirement of a new prerelease version", "v1.2.0-rc1", "v1.2.1-rc2", "v1.1.2", true, ErrPrereleaseCrossVersionUpgrade}, | ||
{"upgrade from a prerelease version to a dev version", "v1.2.2-rc1", "v1.2-ab12cd34", "", true, nil}, | ||
|
||
{"upgrade to same dev version", "v1.2-ab12cd34", "v1.2-ab12cd34", "", true, nil}, | ||
{"upgrade among two dev versions", "v1.2-ab12cd34", "v1.2-1234567", "", true, nil}, | ||
{"upgrade from a dev version to a release version (strict mode)", "v1.2-ab12cd34", "v1.2.1", "v1.1.2", true, ErrDevUpgrade}, | ||
{"upgrade from a dev version to a release version (loose mode)", "v1.2-ab12cd34", "v1.2.1", "v1.1.2", false, nil}, | ||
{"upgrade from a dev version to a prerelease version (strict mode)", "v1.2-ab12cd34", "v1.2.2-rc1", "v1.2.1", true, ErrDevUpgrade}, | ||
{"upgrade from a dev version to a prerelease version (loose mode)", "v1.2-ab12cd34", "v1.2.2-rc1", "v1.2.1", false, nil}, | ||
|
||
{"upgrade from an old release version to a new release version without any minimum requirement specified", "v1.2.1", "v1.3.0", "", true, nil}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
cv, _ := NewHarvesterVersion(tc.currentVersion) | ||
uv, _ := NewHarvesterVersion(tc.upgradeVersion) | ||
mv, _ := NewHarvesterVersion(tc.minUpgradableVersion) | ||
|
||
huv := NewHarvesterUpgradeVersion(cv, uv, mv) | ||
actualErr := huv.CheckUpgradeEligibility(tc.strictMode) | ||
if tc.expectedErr != nil { | ||
assert.Equal(t, tc.expectedErr, actualErr, tc.name) | ||
} else { | ||
assert.Nil(t, actualErr, tc.name) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package version | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
semverv3 "github.com/Masterminds/semver/v3" | ||
) | ||
|
||
type HarvesterVersion struct { | ||
version *semverv3.Version | ||
rawVersion string | ||
isDev bool | ||
isPrerelease bool | ||
} | ||
|
||
func NewHarvesterVersion(versionStr string) (*HarvesterVersion, error) { | ||
var ( | ||
v *semverv3.Version | ||
isDev, isReleaseCandidate bool | ||
) | ||
|
||
trimmedVersionStr := strings.Trim(versionStr, "v") | ||
|
||
// HarvesterVersion accepts any form of version strings except the empty string. | ||
if trimmedVersionStr == "" { | ||
return nil, ErrInvalidVersion | ||
} | ||
|
||
// Version strings other than semantic versions will be treated as dev versions. | ||
// For example, v1.2-head, f024f49a, etc. are dev versions. | ||
v, err := semverv3.StrictNewVersion(trimmedVersionStr) | ||
if err != nil { | ||
isDev = true | ||
} | ||
|
||
if !isDev && v.Prerelease() != "" { | ||
isReleaseCandidate = true | ||
} | ||
|
||
return &HarvesterVersion{ | ||
version: v, | ||
rawVersion: versionStr, | ||
isDev: isDev, | ||
isPrerelease: isReleaseCandidate, | ||
}, nil | ||
} | ||
|
||
// GetStableVersion returns the Version object without the suffix. For example, given "v1.2.2-rc1", it returns "v1.2.2" | ||
func (v *HarvesterVersion) GetStableVersion() *HarvesterVersion { | ||
sv := semverv3.New(v.version.Major(), v.version.Minor(), v.version.Patch(), "", "") | ||
return &HarvesterVersion{ | ||
version: sv, | ||
rawVersion: sv.Original(), | ||
isDev: false, | ||
isPrerelease: false, | ||
} | ||
} | ||
|
||
func (v *HarvesterVersion) IsNewer(version *HarvesterVersion) (bool, error) { | ||
if v.isDev || version.isDev { | ||
return false, ErrIncomparableVersion | ||
} | ||
|
||
var constraint string | ||
|
||
if v.isPrerelease || version.isPrerelease { | ||
constraint = fmt.Sprintf("> %s-z", version.rawVersion) | ||
} else { | ||
constraint = fmt.Sprintf("> %s", version.rawVersion) | ||
} | ||
|
||
c, err := semverv3.NewConstraint(constraint) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return c.Check(v.version), nil | ||
} | ||
|
||
func (v *HarvesterVersion) IsEqual(version *HarvesterVersion) (bool, error) { | ||
if v.isDev || version.isDev { | ||
if v.rawVersion == version.rawVersion { | ||
return true, nil | ||
} | ||
return false, ErrIncomparableVersion | ||
} | ||
|
||
constraint := version.rawVersion | ||
|
||
c, err := semverv3.NewConstraint(constraint) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return c.Check(v.version), nil | ||
} | ||
|
||
func (v *HarvesterVersion) IsOlder(version *HarvesterVersion) (bool, error) { | ||
if v.isDev || version.isDev { | ||
return false, ErrIncomparableVersion | ||
} | ||
|
||
isNewer, err := v.IsNewer(version) | ||
if err != nil { | ||
return false, err | ||
} | ||
isEqual, err := v.IsEqual(version) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return !(isNewer || isEqual), nil | ||
} | ||
|
||
func (v *HarvesterVersion) String() string { | ||
if v.isDev { | ||
return v.rawVersion | ||
} | ||
|
||
return fmt.Sprintf("v%s", v.version.String()) | ||
} |
Oops, something went wrong.