mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
1d5a8283e0
Previously we used strings to pass microservice versions. This invited bugs due to missing input validation.
224 lines
5.8 KiB
Go
224 lines
5.8 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
/*
|
|
Package semver provides functionality to parse and process semantic versions, as they are used in multiple components of Constellation.
|
|
|
|
The official [semantic versioning specification] disallows leading "v" prefixes.
|
|
However, the Constellation config uses the "v" prefix for versions to make version strings more recognizable.
|
|
This package bridges the gap between Go's semver pkg (doesn't allow "v" prefix) and the Constellation config (requires "v" prefix).
|
|
|
|
[semantic versioning specification]: https://semver.org/
|
|
*/
|
|
package semver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/compatibility"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
// Sort sorts a list of semantic version strings using [ByVersion].
|
|
func Sort(list []Semver) {
|
|
sort.Sort(byVersion(list))
|
|
}
|
|
|
|
// Semver represents a semantic version.
|
|
type Semver struct {
|
|
major int
|
|
minor int
|
|
patch int
|
|
prerelease string
|
|
}
|
|
|
|
// New returns a Version from a string.
|
|
func New(version string) (Semver, error) {
|
|
// ensure that semver has "v" prefix
|
|
if !strings.HasPrefix(version, "v") {
|
|
version = "v" + version
|
|
}
|
|
|
|
if !semver.IsValid(version) {
|
|
return Semver{}, fmt.Errorf("invalid semver: %s", version)
|
|
}
|
|
|
|
version = semver.Canonical(version)
|
|
|
|
var major, minor, patch int
|
|
_, pre, _ := strings.Cut(version, "-")
|
|
_, err := fmt.Sscanf(version, "v%d.%d.%d", &major, &minor, &patch)
|
|
if err != nil {
|
|
return Semver{}, fmt.Errorf("parsing semver parts: %w", err)
|
|
}
|
|
|
|
return Semver{
|
|
major: major,
|
|
minor: minor,
|
|
patch: patch,
|
|
prerelease: pre,
|
|
}, nil
|
|
}
|
|
|
|
// NewFromInt constructs a new Semver from three integers and prerelease string: MAJOR.MINOR.PATCH-PRERELEASE.
|
|
func NewFromInt(major, minor, patch int, prerelease string) Semver {
|
|
return Semver{
|
|
major: major,
|
|
minor: minor,
|
|
patch: patch,
|
|
prerelease: prerelease,
|
|
}
|
|
}
|
|
|
|
// NewSlice returns a slice of Semver from a slice of strings.
|
|
func NewSlice(in []string) ([]Semver, error) {
|
|
var out []Semver
|
|
for _, version := range in {
|
|
semVersion, err := New(version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing version %s: %w", version, err)
|
|
}
|
|
out = append(out, semVersion)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// ToStrings converts a slice of Semver to a slice of strings.
|
|
func ToStrings(in []Semver) []string {
|
|
var out []string
|
|
for _, v := range in {
|
|
out = append(out, v.String())
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// Major returns the major version of the object.
|
|
func (v Semver) Major() int {
|
|
return v.major
|
|
}
|
|
|
|
// Minor returns the minor version of the object.
|
|
func (v Semver) Minor() int {
|
|
return v.minor
|
|
}
|
|
|
|
// Patch returns the patch version of the object.
|
|
func (v Semver) Patch() int {
|
|
return v.patch
|
|
}
|
|
|
|
// Prerelease returns the prerelease section of the object.
|
|
func (v Semver) Prerelease() string {
|
|
return v.prerelease
|
|
}
|
|
|
|
// String returns the string representation of the version.
|
|
func (v Semver) String() string {
|
|
version := fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch)
|
|
if v.prerelease != "" {
|
|
return fmt.Sprintf("%s-%s", version, v.prerelease)
|
|
}
|
|
return version
|
|
}
|
|
|
|
// Compare compares two versions. It relies on the semver.Compare function internally.
|
|
func (v Semver) Compare(other Semver) int {
|
|
return semver.Compare(v.String(), other.String())
|
|
}
|
|
|
|
// MajorMinorEqual returns if the major and minor version of two versions are equal.
|
|
func (v Semver) MajorMinorEqual(other Semver) bool {
|
|
return v.major == other.major && v.minor == other.minor
|
|
}
|
|
|
|
// IsUpgradeTo returns if a version is an upgrade to another version.
|
|
// It checks if the version of v is greater than the version of other and allows a drift of at most one minor version.
|
|
func (v Semver) IsUpgradeTo(other Semver) error {
|
|
if v.Compare(other) <= 0 {
|
|
return compatibility.NewInvalidUpgradeError(v.String(), other.String(), errors.New("current version newer than or equal to new version"))
|
|
}
|
|
if v.major != other.major {
|
|
return compatibility.NewInvalidUpgradeError(v.String(), other.String(), compatibility.ErrMajorMismatch)
|
|
}
|
|
|
|
if v.minor-other.minor > 1 {
|
|
return compatibility.NewInvalidUpgradeError(v.String(), other.String(), compatibility.ErrMinorDrift)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextMinor returns the next minor version in the format "vMAJOR.MINOR+1".
|
|
func (v Semver) NextMinor() string {
|
|
return fmt.Sprintf("v%d.%d", v.major, v.minor+1)
|
|
}
|
|
|
|
// MarshalYAML implements the yaml.Marshaller interface.
|
|
func (v Semver) MarshalYAML() (any, error) {
|
|
return v.String(), nil
|
|
}
|
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
func (v *Semver) UnmarshalYAML(unmarshal func(any) error) error {
|
|
var raw string
|
|
if err := unmarshal(&raw); err != nil {
|
|
return fmt.Errorf("unmarshalling to string: %w", err)
|
|
}
|
|
|
|
version, err := New(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing semantic version: %w", err)
|
|
}
|
|
|
|
*v = version
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements the json.Marshaler interface.
|
|
func (v Semver) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf(`"%s"`, v.String())), nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
|
func (v *Semver) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
version, err := New(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*v = version
|
|
return nil
|
|
}
|
|
|
|
// byVersion implements [sort.Interface] for sorting semantic version strings.
|
|
// Copied from Go's semver pkg with minimal modification.
|
|
// https://cs.opensource.google/go/x/mod/+/master:semver/semver.go
|
|
type byVersion []Semver
|
|
|
|
func (vs byVersion) Len() int { return len(vs) }
|
|
func (vs byVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
|
func (vs byVersion) Less(i, j int) bool {
|
|
cmp := vs[i].Compare(vs[j])
|
|
if cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
|
|
// if versions are equal, sort by lexicographic order
|
|
return vs[i].String() < vs[j].String()
|
|
}
|