Skip to content

Commit

Permalink
feat: add version library
Browse files Browse the repository at this point in the history
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
starbops committed Apr 16, 2024
1 parent eb8883e commit bfcd6e3
Show file tree
Hide file tree
Showing 7 changed files with 588 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
)

require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
12 changes: 12 additions & 0 deletions version/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package version

import "errors"

var (
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")
)
88 changes: 88 additions & 0 deletions version/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 {
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
}
157 changes: 157 additions & 0 deletions version/upgrade_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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, err := NewHarvesterVersion(tc.currentVersion)
assert.Nil(t, err, tc.name)

uv, err := NewHarvesterVersion(tc.upgradeVersion)
assert.Nil(t, err, tc.name)

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

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

cv, err := NewHarvesterVersion(tc.currentVersion)
assert.Nil(t, err, tc.name)

mv, err := NewHarvesterVersion(tc.minUpgradableVersion)
assert.Nil(t, err, tc.name)

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

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

cv, err := NewHarvesterVersion(tc.currentVersion)
assert.Nil(t, err, tc.name)

uv, err := NewHarvesterVersion(tc.upgradeVersion)
assert.Nil(t, err, tc.name)

mv, err := NewHarvesterVersion(tc.minUpgradableVersion)
assert.Nil(t, err, tc.name)

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)
}
})
}
}
115 changes: 115 additions & 0 deletions version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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")

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())
}
Loading

0 comments on commit bfcd6e3

Please sign in to comment.