Add a package for handling version numbers (including non-semvers)

pull/6/head
Dan Winship 2016-10-21 12:19:14 -04:00
parent 15f9572b8c
commit bb60f0415a
6 changed files with 541 additions and 0 deletions

View File

@ -266,6 +266,7 @@ pkg/util/ratelimit
pkg/util/replicaset
pkg/util/restoptions
pkg/util/validation/field
pkg/util/version
pkg/util/workqueue
pkg/version/prometheus
pkg/volume

26
pkg/util/version/BUILD Normal file
View File

@ -0,0 +1,26 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"version.go",
],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["version_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [],
)

18
pkg/util/version/doc.go Normal file
View File

@ -0,0 +1,18 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package version provides utilities for version number comparisons
package version // import "k8s.io/kubernetes/pkg/util/version"

236
pkg/util/version/version.go Normal file
View File

@ -0,0 +1,236 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
)
// Version is an opqaue representation of a version number
type Version struct {
components []uint
semver bool
preRelease string
buildMetadata string
}
var (
// versionMatchRE splits a version string into numeric and "extra" parts
versionMatchRE = regexp.MustCompile(`^\s*v?([0-9]+(?:\.[0-9]+)*)(.*)*$`)
// extraMatchRE splits the "extra" part of versionMatchRE into semver pre-release and build metadata; it does not validate the "no leading zeroes" constraint for pre-release
extraMatchRE = regexp.MustCompile(`^(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?\s*$`)
)
func parse(str string, semver bool) (*Version, error) {
parts := versionMatchRE.FindStringSubmatch(str)
if parts == nil {
return nil, fmt.Errorf("could not parse %q as version", str)
}
numbers, extra := parts[1], parts[2]
components := strings.Split(numbers, ".")
if (semver && len(components) != 3) || (!semver && len(components) < 2) {
return nil, fmt.Errorf("illegal version string %q", str)
}
v := &Version{
components: make([]uint, len(components)),
semver: semver,
}
for i, comp := range components {
if (i == 0 || semver) && strings.HasPrefix(comp, "0") && comp != "0" {
return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str)
}
num, err := strconv.ParseUint(comp, 10, 0)
if err != nil {
return nil, fmt.Errorf("illegal non-numeric version component %q in %q: %v", comp, str, err)
}
v.components[i] = uint(num)
}
if semver && extra != "" {
extraParts := extraMatchRE.FindStringSubmatch(extra)
if extraParts == nil {
return nil, fmt.Errorf("could not parse pre-release/metadata (%s) in version %q", extra, str)
}
v.preRelease, v.buildMetadata = extraParts[1], extraParts[2]
for _, comp := range strings.Split(v.preRelease, ".") {
if _, err := strconv.ParseUint(comp, 10, 0); err == nil {
if strings.HasPrefix(comp, "0") && comp != "0" {
return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str)
}
}
}
}
return v, nil
}
// ParseGeneric parses a "generic" version string. The version string must consist of two
// or more dot-separated numeric fields (the first of which can't have leading zeroes),
// followed by arbitrary uninterpreted data (which need not be separated from the final
// numeric field by punctuation). For convenience, leading and trailing whitespace is
// ignored, and the version can be preceded by the letter "v". See also ParseSemantic.
func ParseGeneric(str string) (*Version, error) {
return parse(str, false)
}
// MustParseGeneric is like ParseGeneric except that it panics on error
func MustParseGeneric(str string) *Version {
v, err := ParseGeneric(str)
if err != nil {
panic(err)
}
return v
}
// ParseSemantic parses a version string that exactly obeys the syntax and semantics of
// the "Semantic Versioning" specification (http://semver.org/) (although it ignores
// leading and trailing whitespace, and allows the version to be preceded by "v"). For
// version strings that are not guaranteed to obey the Semantic Versioning syntax, use
// ParseGeneric.
func ParseSemantic(str string) (*Version, error) {
return parse(str, true)
}
// MustParseSemantic is like ParseSemantic except that it panics on error
func MustParseSemantic(str string) *Version {
v, err := ParseSemantic(str)
if err != nil {
panic(err)
}
return v
}
// BuildMetadata returns the build metadata, if v is a Semantic Version, or ""
func (v *Version) BuildMetadata() string {
return v.buildMetadata
}
// String converts a Version back to a string; note that for versions parsed with
// ParseGeneric, this will not include the trailing uninterpreted portion of the version
// number.
func (v *Version) String() string {
var buffer bytes.Buffer
for i, comp := range v.components {
if i > 0 {
buffer.WriteString(".")
}
buffer.WriteString(fmt.Sprintf("%d", comp))
}
if v.preRelease != "" {
buffer.WriteString("-")
buffer.WriteString(v.preRelease)
}
if v.buildMetadata != "" {
buffer.WriteString("+")
buffer.WriteString(v.buildMetadata)
}
return buffer.String()
}
// compareInternal returns -1 if v is less than other, 1 if it is greater than other, or 0
// if they are equal
func (v *Version) compareInternal(other *Version) int {
for i := range v.components {
switch {
case i >= len(other.components):
if v.components[i] != 0 {
return 1
}
case other.components[i] < v.components[i]:
return 1
case other.components[i] > v.components[i]:
return -1
}
}
if !v.semver || !other.semver {
return 0
}
switch {
case v.preRelease == "" && other.preRelease != "":
return 1
case v.preRelease != "" && other.preRelease == "":
return -1
case v.preRelease == other.preRelease: // includes case where both are ""
return 0
}
vPR := strings.Split(v.preRelease, ".")
oPR := strings.Split(other.preRelease, ".")
for i := range vPR {
if i >= len(oPR) {
return 1
}
vNum, err := strconv.ParseUint(vPR[i], 10, 0)
if err == nil {
oNum, err := strconv.ParseUint(oPR[i], 10, 0)
if err == nil {
switch {
case oNum < vNum:
return 1
case oNum > vNum:
return -1
default:
continue
}
}
}
if oPR[i] < vPR[i] {
return 1
} else if oPR[i] > vPR[i] {
return -1
}
}
return 0
}
// AtLeast tests if a version is at least equal to a given minimum version. If both
// Versions are Semantic Versions, this will use the Semantic Version comparison
// algorithm. Otherwise, it will compare only the numeric components, with non-present
// components being considered "0" (ie, "1.4" is equal to "1.4.0").
func (v *Version) AtLeast(min *Version) bool {
return v.compareInternal(min) != -1
}
// LessThan tests if a version is less than a given version. (It is exactly the opposite
// of AtLeast, for situations where asking "is v too old?" makes more sense than asking
// "is v new enough?".)
func (v *Version) LessThan(other *Version) bool {
return v.compareInternal(other) == -1
}
// Compare compares v against a version string (which will be parsed as either Semantic
// or non-Semantic depending on v). On success it returns -1 if v is less than other, 1 if
// it is greater than other, or 0 if they are equal.
func (v *Version) Compare(other string) (int, error) {
ov, err := parse(other, v.semver)
if err != nil {
return 0, err
}
return v.compareInternal(ov), nil
}

View File

@ -0,0 +1,259 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
import (
"fmt"
"testing"
)
type testItem struct {
version string
unparsed string
equalsPrev bool
}
func testOne(v *Version, item, prev testItem) error {
str := v.String()
if item.unparsed == "" {
if str != item.version {
return fmt.Errorf("bad round-trip: %q -> %q", item.version, str)
}
} else {
if str != item.unparsed {
return fmt.Errorf("bad unparse: %q -> %q, expected %q", item.version, str, item.unparsed)
}
}
if prev.version != "" {
cmp, err := v.Compare(prev.version)
if err != nil {
return fmt.Errorf("unexpected parse error: %v", err)
}
switch {
case cmp == -1:
return fmt.Errorf("unexpected ordering %q < %q", item.version, prev.version)
case cmp == 0 && !item.equalsPrev:
return fmt.Errorf("unexpected comparison %q == %q", item.version, item.version)
case cmp == 1 && item.equalsPrev:
return fmt.Errorf("unexpected comparison %q != %q", item.version, item.version)
}
}
return nil
}
func TestSemanticVersions(t *testing.T) {
tests := []testItem{
// This is every version string that appears in the 2.0 semver spec,
// sorted in strictly increasing order except as noted.
{version: "0.1.0"},
{version: "1.0.0-0.3.7"},
{version: "1.0.0-alpha"},
{version: "1.0.0-alpha+001", equalsPrev: true},
{version: "1.0.0-alpha.1"},
{version: "1.0.0-alpha.beta"},
{version: "1.0.0-beta"},
{version: "1.0.0-beta+exp.sha.5114f85", equalsPrev: true},
{version: "1.0.0-beta.2"},
{version: "1.0.0-beta.11"},
{version: "1.0.0-rc.1"},
{version: "1.0.0-x.7.z.92"},
{version: "1.0.0"},
{version: "1.0.0+20130313144700", equalsPrev: true},
{version: "1.9.0"},
{version: "1.10.0"},
{version: "1.11.0"},
{version: "2.0.0"},
{version: "2.1.0"},
{version: "2.1.1"},
{version: "42.0.0"},
// We also allow whitespace and "v" prefix
{version: " 42.0.0", unparsed: "42.0.0", equalsPrev: true},
{version: "\t42.0.0 ", unparsed: "42.0.0", equalsPrev: true},
{version: "43.0.0-1", unparsed: "43.0.0-1"},
{version: "43.0.0-1 ", unparsed: "43.0.0-1", equalsPrev: true},
{version: "v43.0.0-1", unparsed: "43.0.0-1", equalsPrev: true},
{version: " v43.0.0", unparsed: "43.0.0"},
{version: " 43.0.0 ", unparsed: "43.0.0", equalsPrev: true},
}
var prev testItem
for _, item := range tests {
v, err := ParseSemantic(item.version)
if err != nil {
t.Errorf("unexpected parse error: %v", err)
continue
}
err = testOne(v, item, prev)
if err != nil {
t.Errorf("%v", err)
}
prev = item
}
}
func TestBadSemanticVersions(t *testing.T) {
tests := []string{
// "MUST take the form X.Y.Z"
"1",
"1.2",
"1.2.3.4",
".2.3",
"1..3",
"1.2.",
"",
"..",
// "where X, Y, and Z are non-negative integers"
"-1.2.3",
"1.-2.3",
"1.2.-3",
"1a.2.3",
"1.2a.3",
"1.2.3a",
"a1.2.3",
"a.b.c",
"1 .2.3",
"1. 2.3",
// "and MUST NOT contain leading zeroes."
"01.2.3",
"1.02.3",
"1.2.03",
// "[pre-release] identifiers MUST comprise only ASCII alphanumerics and hyphen"
"1.2.3-/",
// "[pre-release] identifiers MUST NOT be empty"
"1.2.3-",
"1.2.3-.",
"1.2.3-foo.",
"1.2.3-.foo",
// "Numeric [pre-release] identifiers MUST NOT include leading zeroes"
"1.2.3-01",
// "[build metadata] identifiers MUST comprise only ASCII alphanumerics and hyphen"
"1.2.3+/",
// "[build metadata] identifiers MUST NOT be empty"
"1.2.3+",
"1.2.3+.",
"1.2.3+foo.",
"1.2.3+.foo",
// whitespace/"v"-prefix checks
"v 1.2.3",
"vv1.2.3",
}
for i := range tests {
_, err := ParseSemantic(tests[i])
if err == nil {
t.Errorf("unexpected success parsing invalid semver %q", tests[i])
}
}
}
func TestGenericVersions(t *testing.T) {
tests := []testItem{
// This is all of the strings from TestSemanticVersions, plus some strings
// from TestBadSemanticVersions that should parse as generic versions,
// plus some additional strings.
{version: "0.1.0", unparsed: "0.1.0"},
{version: "1.0.0-0.3.7", unparsed: "1.0.0"},
{version: "1.0.0-alpha", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0-alpha+001", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0-alpha.1", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0-alpha.beta", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0.beta", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0-beta+exp.sha.5114f85", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0.beta.2", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0.beta.11", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0.rc.1", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0-x.7.z.92", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0", unparsed: "1.0.0", equalsPrev: true},
{version: "1.0.0+20130313144700", unparsed: "1.0.0", equalsPrev: true},
{version: "1.2", unparsed: "1.2"},
{version: "1.2a.3", unparsed: "1.2", equalsPrev: true},
{version: "1.2.3", unparsed: "1.2.3"},
{version: "1.2.3a", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3-foo.", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3-.foo", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3-01", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3+", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3+foo.", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3+.foo", unparsed: "1.2.3", equalsPrev: true},
{version: "1.02.3", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.03", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.003", unparsed: "1.2.3", equalsPrev: true},
{version: "1.2.3.4", unparsed: "1.2.3.4"},
{version: "1.2.3.4b3", unparsed: "1.2.3.4", equalsPrev: true},
{version: "1.2.3.4.5", unparsed: "1.2.3.4.5"},
{version: "1.9.0", unparsed: "1.9.0"},
{version: "1.10.0", unparsed: "1.10.0"},
{version: "1.11.0", unparsed: "1.11.0"},
{version: "2.0.0", unparsed: "2.0.0"},
{version: "2.1.0", unparsed: "2.1.0"},
{version: "2.1.1", unparsed: "2.1.1"},
{version: "42.0.0", unparsed: "42.0.0"},
{version: " 42.0.0", unparsed: "42.0.0", equalsPrev: true},
{version: "\t42.0.0 ", unparsed: "42.0.0", equalsPrev: true},
{version: "42.0.0-1", unparsed: "42.0.0", equalsPrev: true},
{version: "42.0.0-1 ", unparsed: "42.0.0", equalsPrev: true},
{version: "v42.0.0-1", unparsed: "42.0.0", equalsPrev: true},
{version: " v43.0.0", unparsed: "43.0.0"},
{version: " 43.0.0 ", unparsed: "43.0.0", equalsPrev: true},
}
var prev testItem
for _, item := range tests {
v, err := ParseGeneric(item.version)
if err != nil {
t.Errorf("unexpected parse error: %v", err)
continue
}
err = testOne(v, item, prev)
if err != nil {
t.Errorf("%v", err)
}
prev = item
}
}
func TestBadGenericVersions(t *testing.T) {
tests := []string{
"1",
"01.2.3",
"-1.2.3",
"1.-2.3",
".2.3",
"1..3",
"1a.2.3",
"a1.2.3",
"1 .2.3",
"1. 2.3",
"1.bob",
"bob",
"v 1.2.3",
"vv1.2.3",
"",
".",
}
for i := range tests {
_, err := ParseGeneric(tests[i])
if err == nil {
t.Errorf("unexpected success parsing invalid version %q", tests[i])
}
}
}

View File

@ -872,6 +872,7 @@ k8s.io/kubernetes/pkg/util/testing,jlowdermilk,1
k8s.io/kubernetes/pkg/util/threading,roberthbailey,1
k8s.io/kubernetes/pkg/util/validation,Q-Lee,1
k8s.io/kubernetes/pkg/util/validation/field,timstclair,1
k8s.io/kubernetes/pkg/util/version,danwinship,0
k8s.io/kubernetes/pkg/util/wait,Q-Lee,1
k8s.io/kubernetes/pkg/util/workqueue,mtaufen,1
k8s.io/kubernetes/pkg/util/wsstream,timothysc,1

1 name owner auto-assigned
872 k8s.io/kubernetes/pkg/util/threading roberthbailey 1
873 k8s.io/kubernetes/pkg/util/validation Q-Lee 1
874 k8s.io/kubernetes/pkg/util/validation/field timstclair 1
875 k8s.io/kubernetes/pkg/util/version danwinship 0
876 k8s.io/kubernetes/pkg/util/wait Q-Lee 1
877 k8s.io/kubernetes/pkg/util/workqueue mtaufen 1
878 k8s.io/kubernetes/pkg/util/wsstream timothysc 1