Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ require (
github.com/sirupsen/logrus v1.10.1 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/grpc v1.65.1 // indirect
Expand Down
2 changes: 0 additions & 2 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/stretchr/testify v1.12.1
github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/mod v0.32.0
golang.org/x/sys v0.39.0
Comment thread
tariq1890 marked this conversation as resolved.
google.golang.org/grpc v1.65.1
google.golang.org/protobuf v1.34.1
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
Expand Down
221 changes: 210 additions & 11 deletions pkg/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
package version

import (
"fmt"
"runtime/debug"
"slices"
"strconv"
"strings"

"golang.org/x/mod/semver"
)

const (
Expand All @@ -34,6 +34,203 @@ const (
nriModulePath = "github.com/containerd/nri"
)

// version represents a struct type that holds relevant data
// that constitute a Semantic Version
type version struct {
major string
minor string
patch string
pre string
}

func (v version) String() string {
if v.pre != "" {
return fmt.Sprintf("v%s.%s.%s-%s", v.major, v.minor, v.patch, v.pre)
}
return fmt.Sprintf("v%s.%s.%s", v.major, v.minor, v.patch)
}

// compareVersion parses two semver strings into the "version" struct type and compares them.
// NOTE: It ignores the build metadata when making the comparison
func compareVersion(a, b string) int {
aVer, errA := parseVersion(a)
bVer, errB := parseVersion(b)
// Like semver.Compare, order invalid versions before valid ones
// and consider any two invalid versions equal.
switch {
case errA != nil && errB != nil:
return 0
case errA != nil:
return -1
case errB != nil:
return 1
}

if c := compareInt(aVer.major, bVer.major); c != 0 {
return c
}
if c := compareInt(aVer.minor, bVer.minor); c != 0 {
return c
}
if c := compareInt(aVer.patch, bVer.patch); c != 0 {
return c
}
return comparePrerelease(aVer.pre, bVer.pre)
}

func compareInt(x, y string) int {
if x == y {
return 0
}
if len(x) < len(y) {
return -1
}
if len(x) > len(y) {
return 1
}
if x < y {
return -1
}
return 1
}

func comparePrerelease(x, y string) int {
if x == y {
return 0
}
if x == "" {
return 1
}
if y == "" {
return -1
}
for x != "" && y != "" {
var dx, dy string
dx, x, _ = strings.Cut(x, ".")
dy, y, _ = strings.Cut(y, ".")
if dx != dy {
ix := isNum(dx)
iy := isNum(dy)
if ix != iy {
if ix {
return -1
}
return 1
}
if ix {
if len(dx) < len(dy) {
return -1
}
if len(dx) > len(dy) {
return 1
}
}
if dx < dy {
return -1
}
return 1
}
}
if x == "" {
return -1
}
return 1
}

func isNum(v string) bool {
if len(v) == 0 {
return false
}
i := 0
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
i++
}
return i == len(v)
}

// parseVersion parses a semantic version string. Like the semver package it replaces, it requires a leading "v"
// and accepts the shorthand forms v<Major> and v<Major>.<Minor> (pre-release and build metadata are only allowed
// on a full v<Major>.<Minor>.<Patch> version). Build metadata is validated but discarded.
func parseVersion(s string) (version, error) {
rest, ok := strings.CutPrefix(s, "v")
if !ok {
return version{}, fmt.Errorf("invalid version %q: missing 'v' prefix", s)
}

rest, build, hasBuild := strings.Cut(rest, "+")
if hasBuild {
if err := validateIdentifiers(build, false); err != nil {
return version{}, fmt.Errorf("invalid build metadata in %q: %w", s, err)
}
}

core, pre, hasPre := strings.Cut(rest, "-")
if hasPre {
if err := validateIdentifiers(pre, true); err != nil {
return version{}, fmt.Errorf("invalid pre-release in %q: %w", s, err)
}
}

parts := strings.Split(core, ".")
if len(parts) > 3 {
return version{}, fmt.Errorf("invalid version %q: too many components", s)
}
if len(parts) < 3 && (hasPre || hasBuild) {
return version{}, fmt.Errorf("invalid version %q: pre-release or build metadata requires major.minor.patch", s)
}

var nums [3]string
for i, part := range parts {
n, err := parseNumber(part)
if err != nil {
return version{}, fmt.Errorf("invalid version %q: %w", s, err)
}
nums[i] = n
}
for i := range nums {
if nums[i] == "" {
nums[i] = "0"
}
}

return version{major: nums[0], minor: nums[1], patch: nums[2], pre: pre}, nil
}

// parseNumber parses a non-negative decimal number without leading zeros.
func parseNumber(s string) (string, error) {
if s == "" || !isNum(s) {
return "", fmt.Errorf("invalid number %q", s)
}
if len(s) > 1 && s[0] == '0' {
return "", fmt.Errorf("number %q has a leading zero", s)
}
return s, nil
}

// validateIdentifiers checks a dot-separated list of pre-release or build identifiers:
// each must be non-empty and consist of [0-9A-Za-z-]. Numeric pre-release identifiers must not have leading zeros.
func validateIdentifiers(s string, pre bool) error {
for id := range strings.SplitSeq(s, ".") {
if id == "" {
return fmt.Errorf("empty identifier in %q", s)
}
for i := 0; i < len(id); i++ {
c := id[i]
if !isValidIdentifierChar(c) {
return fmt.Errorf("invalid character %q in identifier %q", c, id)
}
}
if pre && isNum(id) && len(id) > 1 && id[0] == '0' {
return fmt.Errorf("numeric identifier %q has a leading zero", id)
}
}
return nil
}

func isValidIdentifierChar(c byte) bool {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-'
}

// GetFromBuildInfo returns the locally used NRI version. This
// is taken either from the debug/build info provided by the
// golang runtime, or for plugins hosted in the NRI repository
Expand Down Expand Up @@ -63,8 +260,8 @@ func GetFromBuildInfo() string {
}

// majorMinorPatch returns the major.minor.patch prefix of the semantic version v.
func majorMinorPatch(v string) string {
return strings.TrimSuffix(strings.TrimSuffix(v, semver.Build(v)), semver.Prerelease(v))
func majorMinorPatch(v version) string {
return fmt.Sprintf("v%s.%s.%s", v.major, v.minor, v.patch)
}

// FindClosestMatch returns the largest version smaller or equal to a given one.
Expand All @@ -76,11 +273,12 @@ func FindClosestMatch(v string, versions []string) string {
// obviously not the case. In lack of a better choice, we strip any such
// suffix from v before comparison.
v = stripGitSuffix(v)
semver.Sort(versions)

slices.SortFunc(versions, compareVersion)

latest := ""
for _, ver := range versions {
if semver.Compare(ver, v) > 0 {
if compareVersion(ver, v) > 0 {
break
}
latest = ver
Expand All @@ -92,17 +290,18 @@ func FindClosestMatch(v string, versions []string) string {
// We expect a valid git suffix to be of the form "-N-gSHA1[.m], where
// N is an decimal integer and SHA1 is a hexadecimal integer.
func stripGitSuffix(version string) string {
mmp := majorMinorPatch(version)
pre := semver.Prerelease(version)
if mmp+pre != version {
pv, _ := parseVersion(version)
mmp := majorMinorPatch(pv)
if pv.String() != version {
return version
}

if len(pre) == 0 || pre[0] != '-' {
pre := pv.pre
if len(pre) == 0 {
return version
}

commits, gsha1, ok := strings.Cut(pre[1:], "-")
commits, gsha1, ok := strings.Cut(pre, "-")
if !ok || len(gsha1) == 0 || gsha1[0] != 'g' {
return version
}
Expand Down
Loading
Loading