constellation/internal/compatibility/compatibility.go
Daniel Weiße d95ddd01d3
helm: fix upgrade command unintentionally skipping all service upgrades (#1992)
* Fix usage of errors.As in upgrade command implementation

* Use struct pointers when working with custom errors

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
2023-06-30 16:46:05 +02:00

164 lines
4.9 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package compatibility offers helper functions for comparing and filtering versions.
*/
package compatibility
import (
"errors"
"fmt"
"strings"
"golang.org/x/mod/semver"
)
var (
// ErrMajorMismatch signals that the major version of two compared versions don't match.
ErrMajorMismatch = errors.New("missmatching major version")
// ErrMinorDrift signals that the minor version of two compared versions are further apart than one.
ErrMinorDrift = errors.New("version difference larger than one minor version")
// ErrSemVer signals that a given version does not adhere to the Semver syntax.
ErrSemVer = errors.New("invalid semantic version")
// ErrOutdatedCLI signals that the configured version is newer than the CLI. This is not allowed.
ErrOutdatedCLI = errors.New("target version newer than cli version")
)
// InvalidUpgradeError present an invalid upgrade. It wraps the source and destination version for improved debuggability.
type InvalidUpgradeError struct {
from string
to string
innerErr error
}
// NewInvalidUpgradeError returns a new InvalidUpgradeError.
func NewInvalidUpgradeError(from string, to string, innerErr error) *InvalidUpgradeError {
return &InvalidUpgradeError{from: from, to: to, innerErr: innerErr}
}
// Unwrap returns the inner error, which is nil in this case.
func (e *InvalidUpgradeError) Unwrap() error {
return e.innerErr
}
// Error returns the String representation of this error.
func (e *InvalidUpgradeError) Error() string {
return fmt.Sprintf("upgrading from %s to %s is not a valid upgrade: %s", e.from, e.to, e.innerErr)
}
// EnsurePrefixV returns the input string prefixed with the letter "v", if the string doesn't already start with that letter.
func EnsurePrefixV(str string) string {
if strings.HasPrefix(str, "v") {
return str
}
return "v" + str
}
// IsValidUpgrade checks that a and b adhere to a version drift of 1 and b is greater than a.
func IsValidUpgrade(a, b string) error {
a = EnsurePrefixV(a)
b = EnsurePrefixV(b)
if !semver.IsValid(a) || !semver.IsValid(b) {
return NewInvalidUpgradeError(a, b, ErrSemVer)
}
if semver.Compare(a, b) >= 0 {
return NewInvalidUpgradeError(a, b, errors.New("current version newer than or equal to new version"))
}
aMajor, aMinor, err := parseCanonicalSemver(a)
if err != nil {
return NewInvalidUpgradeError(a, b, err)
}
bMajor, bMinor, err := parseCanonicalSemver(b)
if err != nil {
return NewInvalidUpgradeError(a, b, err)
}
if aMajor != bMajor {
return NewInvalidUpgradeError(a, b, ErrMajorMismatch)
}
if bMinor-aMinor > 1 {
return NewInvalidUpgradeError(a, b, ErrMinorDrift)
}
return nil
}
// BinaryWith tests that this binarie's version is greater or equal than some target version, but not further away than one minor version.
func BinaryWith(binaryVersion, target string) error {
binaryVersion = EnsurePrefixV(binaryVersion)
target = EnsurePrefixV(target)
if !semver.IsValid(binaryVersion) || !semver.IsValid(target) {
return ErrSemVer
}
cliMajor, cliMinor, err := parseCanonicalSemver(binaryVersion)
if err != nil {
return err
}
targetMajor, targetMinor, err := parseCanonicalSemver(target)
if err != nil {
return err
}
// Major versions always have to match.
if cliMajor != targetMajor {
return ErrMajorMismatch
}
// For images we allow newer patch versions, therefore this only checks the minor version.
if semver.Compare(semver.MajorMinor(binaryVersion), semver.MajorMinor(target)) == -1 {
return ErrOutdatedCLI
}
// Abort if minor version drift between CLI and versionA value is greater than 1.
if cliMinor-targetMinor > 1 {
return ErrMinorDrift
}
return nil
}
// FilterNewerVersion filters the list of versions to only include versions newer than currentVersion.
func FilterNewerVersion(currentVersion string, newVersions []string) []string {
currentVersion = EnsurePrefixV(currentVersion)
var result []string
for _, image := range newVersions {
image = EnsurePrefixV(image)
// check if image is newer than current version
if semver.Compare(image, currentVersion) <= 0 {
continue
}
result = append(result, image)
}
return result
}
// NextMinorVersion returns the next minor version for a given canonical semver.
// The returned format is vMAJOR.MINOR.
func NextMinorVersion(version string) (string, error) {
major, minor, err := parseCanonicalSemver(EnsurePrefixV(version))
if err != nil {
return "", err
}
return fmt.Sprintf("v%d.%d", major, minor+1), nil
}
func parseCanonicalSemver(version string) (major int, minor int, err error) {
version = semver.MajorMinor(version) // ensure version is in canonical form (vX.Y.Z)
if version == "" {
return 0, 0, fmt.Errorf("invalid semver: '%s'", version)
}
_, err = fmt.Sscanf(version, "v%d.%d", &major, &minor)
if err != nil {
return 0, 0, fmt.Errorf("parsing version: %w", err)
}
return major, minor, nil
}