sigs.k8s.io/cluster-api@v1.7.1/util/version/version.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package version implements version handling.
    18  package version
    19  
    20  import (
    21  	"regexp"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/blang/semver/v4"
    26  	"github.com/pkg/errors"
    27  )
    28  
    29  var (
    30  	// KubeSemver is the regex for Kubernetes versions. It requires the "v" prefix.
    31  	KubeSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
    32  	// KubeSemverTolerant is the regex for Kubernetes versions with an optional "v" prefix.
    33  	KubeSemverTolerant = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
    34  )
    35  
    36  // ParseMajorMinorPatch returns a semver.Version from the string provided
    37  // by looking only at major.minor.patch and stripping everything else out.
    38  // It requires the version to have a "v" prefix.
    39  func ParseMajorMinorPatch(version string) (semver.Version, error) {
    40  	return parseMajorMinorPatch(version, false)
    41  }
    42  
    43  // ParseMajorMinorPatchTolerant returns a semver.Version from the string provided
    44  // by looking only at major.minor.patch and stripping everything else out.
    45  // It does not require the version to have a "v" prefix.
    46  func ParseMajorMinorPatchTolerant(version string) (semver.Version, error) {
    47  	return parseMajorMinorPatch(version, true)
    48  }
    49  
    50  // parseMajorMinorPatch returns a semver.Version from the string provided
    51  // by looking only at major.minor.patch and stripping everything else out.
    52  func parseMajorMinorPatch(version string, tolerant bool) (semver.Version, error) {
    53  	groups := KubeSemver.FindStringSubmatch(version)
    54  	if tolerant {
    55  		groups = KubeSemverTolerant.FindStringSubmatch(version)
    56  	}
    57  	if len(groups) < 4 {
    58  		return semver.Version{}, errors.Errorf("failed to parse major.minor.patch from %q", version)
    59  	}
    60  	major, err := strconv.ParseUint(groups[1], 10, 64)
    61  	if err != nil {
    62  		return semver.Version{}, errors.Wrapf(err, "failed to parse major version from %q", version)
    63  	}
    64  	minor, err := strconv.ParseUint(groups[2], 10, 64)
    65  	if err != nil {
    66  		return semver.Version{}, errors.Wrapf(err, "failed to parse minor version from %q", version)
    67  	}
    68  	patch, err := strconv.ParseUint(groups[3], 10, 64)
    69  	if err != nil {
    70  		return semver.Version{}, errors.Wrapf(err, "failed to parse patch version from %q", version)
    71  	}
    72  	return semver.Version{
    73  		Major: major,
    74  		Minor: minor,
    75  		Patch: patch,
    76  	}, nil
    77  }
    78  
    79  const (
    80  	numbers = "01234567890"
    81  )
    82  
    83  func containsOnly(s string, set string) bool {
    84  	return strings.IndexFunc(s, func(r rune) bool {
    85  		return !strings.ContainsRune(set, r)
    86  	}) == -1
    87  }
    88  
    89  type buildIdentifiers []buildIdentifier
    90  
    91  func newBuildIdentifiers(ids []string) buildIdentifiers {
    92  	bis := make(buildIdentifiers, 0, len(ids))
    93  	for _, id := range ids {
    94  		bis = append(bis, newBuildIdentifier(id))
    95  	}
    96  	return bis
    97  }
    98  
    99  // compare compares 2 builidentifiers v and 0.
   100  // -1 == v is less than o.
   101  // 0 == v is equal to o.
   102  // 1 == v is greater than o.
   103  // Note: If everything else is equal the longer build identifier is greater.
   104  func (v buildIdentifiers) compare(o buildIdentifiers) int {
   105  	i := 0
   106  	for ; i < len(v) && i < len(o); i++ {
   107  		comp := v[i].compare(o[i])
   108  		if comp != 0 {
   109  			return comp
   110  		}
   111  	}
   112  
   113  	// if everything is equal till now the longer is greater
   114  	if i == len(v) && i == len(o) { //nolint: gocritic
   115  		return 0
   116  	} else if i == len(v) && i < len(o) {
   117  		return -1
   118  	}
   119  
   120  	return 1
   121  }
   122  
   123  type buildIdentifier struct {
   124  	IdentifierInt uint64
   125  	IdentifierStr string
   126  	IsNum         bool
   127  }
   128  
   129  func newBuildIdentifier(s string) buildIdentifier {
   130  	bi := buildIdentifier{}
   131  	if containsOnly(s, numbers) {
   132  		num, _ := strconv.ParseUint(s, 10, 64)
   133  		bi.IdentifierInt = num
   134  		bi.IsNum = true
   135  	} else {
   136  		bi.IdentifierStr = s
   137  		bi.IsNum = false
   138  	}
   139  	return bi
   140  }
   141  
   142  // compare compares v and o.
   143  // -1 == v is less than o.
   144  // 0 == v is equal to o.
   145  // 1 == v is greater than o.
   146  // 2 == v is different than o (it is not possible to identify if lower or greater).
   147  // Note: number is considered lower than string.
   148  func (v buildIdentifier) compare(o buildIdentifier) int {
   149  	if v.IsNum && !o.IsNum {
   150  		return -1
   151  	}
   152  	if !v.IsNum && o.IsNum {
   153  		return 1
   154  	}
   155  	if v.IsNum && o.IsNum { // both are numbers
   156  		switch {
   157  		case v.IdentifierInt < o.IdentifierInt:
   158  			return -1
   159  		case v.IdentifierInt == o.IdentifierInt:
   160  			return 0
   161  		default:
   162  			return 1
   163  		}
   164  	} else { // both are strings
   165  		if v.IdentifierStr == o.IdentifierStr {
   166  			return 0
   167  		}
   168  		// In order to support random build identifiers, like commit hashes,
   169  		// we return 2 when the strings are different to signal the
   170  		// build identifiers are different but we can't determine the precedence
   171  		return 2
   172  	}
   173  }
   174  
   175  type comparer struct {
   176  	buildTags          bool
   177  	withoutPreReleases bool
   178  }
   179  
   180  // CompareOption is a configuration option for Compare.
   181  type CompareOption func(*comparer)
   182  
   183  // WithBuildTags modifies the version comparison to also consider build tags
   184  // when comparing versions.
   185  // Performs a standard version compare between a and b. If the versions
   186  // are equal, build identifiers will be used to compare further; precedence for two build
   187  // identifiers is determined by comparing each dot-separated identifier from left to right
   188  // until a difference is found as follows:
   189  // - Identifiers consisting of only digits are compared numerically.
   190  // - Numeric identifiers always have lower precedence than non-numeric identifiers.
   191  // - Identifiers with letters or hyphens are compared only for equality, otherwise, 2 is returned given
   192  // that it is not possible to identify if lower or greater (non-numeric identifiers could be random build
   193  // identifiers).
   194  //
   195  //	-1 == a is less than b.
   196  //	0 == a is equal to b.
   197  //	1 == a is greater than b.
   198  //	2 == v is different than o (it is not possible to identify if lower or greater).
   199  func WithBuildTags() CompareOption {
   200  	return func(c *comparer) {
   201  		c.buildTags = true
   202  	}
   203  }
   204  
   205  // WithoutPreReleases modifies the version comparison to not consider pre-releases
   206  // when comparing versions.
   207  func WithoutPreReleases() CompareOption {
   208  	return func(c *comparer) {
   209  		c.withoutPreReleases = true
   210  	}
   211  }
   212  
   213  // Compare 2 semver versions.
   214  // Defaults to doing the standard semver comparison when no options are specified.
   215  // The comparison logic can be modified by passing additional compare options.
   216  // Example: using the WithBuildTags() option modifies the compare logic to also
   217  // consider build tags when comparing versions.
   218  func Compare(a, b semver.Version, options ...CompareOption) int {
   219  	c := &comparer{}
   220  	for _, o := range options {
   221  		o(c)
   222  	}
   223  
   224  	if c.withoutPreReleases {
   225  		a.Pre = nil
   226  		b.Pre = nil
   227  	}
   228  
   229  	if c.buildTags {
   230  		if comp := a.Compare(b); comp != 0 {
   231  			return comp
   232  		}
   233  		biA := newBuildIdentifiers(a.Build)
   234  		biB := newBuildIdentifiers(b.Build)
   235  		return biA.compare(biB)
   236  	}
   237  	return a.Compare(b)
   238  }