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 }