V2 API Version Support

* Update blang/semver to allow ParseTolerant() support
* Provide helper functions for API handlers to obtain client's 'version'
  path variable focused on API endpoint tree: libpod vs. compat
* Introduce new errors:
  * version not given in path, endpoints may determine if this is a hard
    error (ErrVersionNotGiven)
  * given version not supported (ErrVersionNotSupported), only a soft
    error if the handler is going to hijack the connection
* Added unit tests for version parsing
* bindings check version on connect:
  * client <= Server API version connection is continued
  * client >= Server API version connection fails

Signed-off-by: Jhon Honce <jhonce@redhat.com>
This commit is contained in:
Jhon Honce 2020-05-18 18:05:02 -07:00
parent 09f8f14b4f
commit f9c392f50a
17 changed files with 544 additions and 49 deletions

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/blang/semver v3.5.1+incompatible
github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37
github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect

2
go.sum
View file

@ -39,6 +39,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.1.0+incompatible h1:7hqmJYuaEK3qwVjWubYiht3j93YI0WQBuysxHIfUriU=
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 h1:uxxtrnACqI9zK4ENDMf0WpXfUsHP5V8liuq5QdgDISU=
github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=

View file

@ -5,22 +5,22 @@ import (
"net/http"
"github.com/containers/buildah"
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/api/handlers/utils"
)
// Ping returns headers to client about the service
//
// This handler must always be the same for the compatibility and libpod URL trees!
// Clients will use the Header availability to test which backend engine is in use.
// Note: Additionally handler supports GET and HEAD methods
func Ping(w http.ResponseWriter, r *http.Request) {
w.Header().Set("API-Version", handlers.DefaultApiVersion)
w.Header().Set("API-Version", utils.ApiVersion[utils.CompatTree][utils.CurrentApiVersion].String())
w.Header().Set("BuildKit-Version", "")
w.Header().Set("Docker-Experimental", "true")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Pragma", "no-cache")
// API-Version and Libpod-API-Version may not always be equal
w.Header().Set("Libpod-API-Version", handlers.DefaultApiVersion)
w.Header().Set("Libpod-API-Version", utils.ApiVersion[utils.LibpodTree][utils.CurrentApiVersion].String())
w.Header().Set("Libpod-Buildha-Version", buildah.Version)
w.WriteHeader(http.StatusOK)

View file

@ -8,7 +8,6 @@ import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/containers/libpod/pkg/domain/entities"
docker "github.com/docker/docker/api/types"
@ -35,34 +34,35 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) {
Name: "Podman Engine",
Version: versionInfo.Version,
Details: map[string]string{
"APIVersion": handlers.DefaultApiVersion,
"APIVersion": utils.ApiVersion[utils.LibpodTree][utils.CurrentApiVersion].String(),
"Arch": goRuntime.GOARCH,
"BuildTime": time.Unix(versionInfo.Built, 0).Format(time.RFC3339),
"Experimental": "true",
"GitCommit": versionInfo.GitCommit,
"GoVersion": versionInfo.GoVersion,
"KernelVersion": infoData.Host.Kernel,
"MinAPIVersion": handlers.MinimalApiVersion,
"MinAPIVersion": utils.ApiVersion[utils.LibpodTree][utils.MinimalApiVersion].String(),
"Os": goRuntime.GOOS,
},
}}
utils.WriteResponse(w, http.StatusOK, entities.ComponentVersion{Version: docker.Version{
Platform: struct {
Name string
}{
Name: fmt.Sprintf("%s/%s/%s-%s", goRuntime.GOOS, goRuntime.GOARCH, infoData.Host.Distribution.Distribution, infoData.Host.Distribution.Version),
},
APIVersion: components[0].Details["APIVersion"],
Arch: components[0].Details["Arch"],
BuildTime: components[0].Details["BuildTime"],
Components: components,
Experimental: true,
GitCommit: components[0].Details["GitCommit"],
GoVersion: components[0].Details["GoVersion"],
KernelVersion: components[0].Details["KernelVersion"],
MinAPIVersion: components[0].Details["MinAPIVersion"],
Os: components[0].Details["Os"],
Version: components[0].Version,
}})
utils.WriteResponse(w, http.StatusOK, entities.ComponentVersion{
Version: docker.Version{
Platform: struct {
Name string
}{
Name: fmt.Sprintf("%s/%s/%s-%s", goRuntime.GOOS, goRuntime.GOARCH, infoData.Host.Distribution.Distribution, infoData.Host.Distribution.Version),
},
APIVersion: components[0].Details["APIVersion"],
Arch: components[0].Details["Arch"],
BuildTime: components[0].Details["BuildTime"],
Components: components,
Experimental: true,
GitCommit: components[0].Details["GitCommit"],
GoVersion: components[0].Details["GoVersion"],
KernelVersion: components[0].Details["KernelVersion"],
MinAPIVersion: components[0].Details["MinAPIVersion"],
Os: components[0].Details["Os"],
Version: components[0].Version,
}})
}

View file

@ -1,6 +0,0 @@
package handlers
const (
DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/
MinimalApiVersion = "1.24"
)

View file

@ -9,11 +9,55 @@ import (
"os"
"strings"
"github.com/blang/semver"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type (
// VersionTree determines which API endpoint tree for version
VersionTree int
// VersionLevel determines which API level, current or something from the past
VersionLevel int
)
const (
// LibpodTree supports Libpod endpoints
LibpodTree = VersionTree(iota)
// CompatTree supports Libpod endpoints
CompatTree
// CurrentApiVersion announces what is the current API level
CurrentApiVersion = VersionLevel(iota)
// MinimalApiVersion announces what is the oldest API level supported
MinimalApiVersion
)
var (
// See https://docs.docker.com/engine/api/v1.40/
// libpod compat handlers are expected to honor docker API versions
// ApiVersion provides the current and minimal API versions for compat and libpod endpoint trees
// Note: GET|HEAD /_ping is never versioned and provides the API-Version and Libpod-API-Version headers to allow
// clients to shop for the Version they wish to support
ApiVersion = map[VersionTree]map[VersionLevel]semver.Version{
LibpodTree: {
CurrentApiVersion: semver.MustParse("1.0.0"),
MinimalApiVersion: semver.MustParse("1.0.0"),
},
CompatTree: {
CurrentApiVersion: semver.MustParse("1.40.0"),
MinimalApiVersion: semver.MustParse("1.24.0"),
},
}
// ErrVersionNotGiven returned when version not given by client
ErrVersionNotGiven = errors.New("version not given in URL path")
// ErrVersionNotSupported returned when given version is too old
ErrVersionNotSupported = errors.New("given version is not supported")
)
// IsLibpodRequest returns true if the request related to a libpod endpoint
// (e.g., /v2/libpod/...).
func IsLibpodRequest(r *http.Request) bool {
@ -21,6 +65,48 @@ func IsLibpodRequest(r *http.Request) bool {
return len(split) >= 3 && split[2] == "libpod"
}
// SupportedVersion validates that the version provided by client is included in the given condition
// https://github.com/blang/semver#ranges provides the details for writing conditions
// If a version is not given in URL path, ErrVersionNotGiven is returned
func SupportedVersion(r *http.Request, condition string) (semver.Version, error) {
version := semver.Version{}
val, ok := mux.Vars(r)["version"]
if !ok {
return version, ErrVersionNotGiven
}
safeVal, err := url.PathUnescape(val)
if err != nil {
return version, errors.Wrapf(err, "unable to unescape given API version: %q", val)
}
version, err = semver.ParseTolerant(safeVal)
if err != nil {
return version, errors.Wrapf(err, "unable to parse given API version: %q from %q", safeVal, val)
}
inRange, err := semver.ParseRange(condition)
if err != nil {
return version, err
}
if inRange(version) {
return version, nil
}
return version, ErrVersionNotSupported
}
// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server
// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL
func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) {
tree := CompatTree
if IsLibpodRequest(r) {
tree = LibpodTree
}
return SupportedVersion(r,
fmt.Sprintf(">=%s <=%s", ApiVersion[tree][MinimalApiVersion].String(),
ApiVersion[tree][CurrentApiVersion].String()))
}
// WriteResponse encodes the given value as JSON or string and renders it for http client
func WriteResponse(w http.ResponseWriter, code int, value interface{}) {
// RFC2616 explicitly states that the following status codes "MUST NOT

View file

@ -0,0 +1,139 @@
package utils
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
)
func TestSupportedVersion(t *testing.T) {
req, err := http.NewRequest("GET",
fmt.Sprintf("/v%s/libpod/testing/versions", ApiVersion[LibpodTree][CurrentApiVersion]),
nil)
if err != nil {
t.Fatal(err)
}
req = mux.SetURLVars(req, map[string]string{"version": ApiVersion[LibpodTree][CurrentApiVersion].String()})
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := SupportedVersionWithDefaults(r)
switch {
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
default: // all good
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := `OK`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %q want %q",
rr.Body.String(), expected)
}
}
func TestUnsupportedVersion(t *testing.T) {
version := "999.999.999"
req, err := http.NewRequest("GET",
fmt.Sprintf("/v%s/libpod/testing/versions", version),
nil)
if err != nil {
t.Fatal(err)
}
req = mux.SetURLVars(req, map[string]string{"version": version})
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := SupportedVersionWithDefaults(r)
switch {
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
default: // all good
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusBadRequest)
}
// Check the response body is what we expect.
expected := ErrVersionNotSupported.Error()
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %q want %q",
rr.Body.String(), expected)
}
}
func TestEqualVersion(t *testing.T) {
version := "1.30.0"
req, err := http.NewRequest("GET",
fmt.Sprintf("/v%s/libpod/testing/versions", version),
nil)
if err != nil {
t.Fatal(err)
}
req = mux.SetURLVars(req, map[string]string{"version": version})
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := SupportedVersion(r, "=="+version)
switch {
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err.Error())
default: // all good
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := http.StatusText(http.StatusOK)
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %q want %q",
rr.Body.String(), expected)
}
}

View file

@ -8,6 +8,10 @@
package bindings
import (
"github.com/blang/semver"
)
var (
// PTrue is a convenience variable that can be used in bindings where
// a pointer to a bool (optional parameter) is required.
@ -17,4 +21,7 @@ var (
// a pointer to a bool (optional parameter) is required.
pFalse = false
PFalse = &pFalse
// _*YES*- podman will fail to run if this value is wrong
APIVersion = semver.MustParse("1.0.0")
)

View file

@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/blang/semver"
"github.com/containers/libpod/pkg/api/types"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
@ -143,7 +144,7 @@ func tcpClient(_url *url.URL) (Connection, error) {
}
// pingNewConnection pings to make sure the RESTFUL service is up
// and running. it should only be used where initializing a connection
// and running. it should only be used when initializing a connection
func pingNewConnection(ctx context.Context) error {
client, err := GetClient(ctx)
if err != nil {
@ -154,8 +155,20 @@ func pingNewConnection(ctx context.Context) error {
if err != nil {
return err
}
if response.StatusCode == http.StatusOK {
return nil
v, err := semver.ParseTolerant(response.Header.Get("Libpod-API-Version"))
if err != nil {
return err
}
switch APIVersion.Compare(v) {
case 1, 0:
// Server's job when client version is equal or older
return nil
case -1:
return errors.Errorf("server API version is too old. client %q server %q", APIVersion.String(), v.String())
}
}
return errors.Errorf("ping response was %q", response.StatusCode)
}

View file

@ -1,3 +0,0 @@
package bindings
func (c Connection) Version() {}

View file

@ -10,13 +10,13 @@ t HEAD /_ping 200
t GET /libpod/_ping 200 OK
for i in /version version; do
t GET $i 200 \
.Components[0].Name="Podman Engine" \
.Components[0].Details.APIVersion=1.40 \
.Components[0].Details.MinAPIVersion=1.24 \
.Components[0].Details.Os=linux \
.ApiVersion=1.40 \
.MinAPIVersion=1.24 \
t GET $i 200 \
.Components[0].Name="Podman Engine" \
.Components[0].Details.APIVersion=1.0.0 \
.Components[0].Details.MinAPIVersion=1.0.0 \
.Components[0].Details.Os=linux \
.ApiVersion=1.0.0 \
.MinAPIVersion=1.0.0 \
.Os=linux
done

21
vendor/github.com/blang/semver/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,21 @@
language: go
matrix:
include:
- go: 1.4.3
- go: 1.5.4
- go: 1.6.3
- go: 1.7
- go: tip
allow_failures:
- go: tip
install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- echo "Test and track coverage" ; $HOME/gopath/bin/goveralls -package "." -service=travis-ci
-repotoken $COVERALLS_TOKEN
- echo "Build examples" ; cd examples && go build
- echo "Check if gofmt'd" ; diff -u <(echo -n) <(gofmt -d -s .)
env:
global:
secure: HroGEAUQpVq9zX1b1VIkraLiywhGbzvNnTZq2TMxgK7JHP8xqNplAeF1izrR2i4QLL9nsY+9WtYss4QuPvEtZcVHUobw6XnL6radF7jS1LgfYZ9Y7oF+zogZ2I5QUMRLGA7rcxQ05s7mKq3XZQfeqaNts4bms/eZRefWuaFZbkw=

View file

@ -1,4 +1,4 @@
semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master)
semver for golang [![Build Status](https://travis-ci.org/blang/semver.svg?branch=master)](https://travis-ci.org/blang/semver) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master)
======
semver is a [Semantic Versioning](http://semver.org/) library written in golang. It fully covers spec version `2.0.0`.
@ -41,6 +41,7 @@ Features
- Compare Helper Methods
- InPlace manipulation
- Ranges `>=1.0.0 <2.0.0 || >=3.0.0 !3.0.1-beta.1`
- Wildcards `>=1.x`, `<=2.5.x`
- Sortable (implements sort.Interface)
- database/sql compatible (sql.Scanner/Valuer)
- encoding/json compatible (json.Marshaler/Unmarshaler)
@ -59,6 +60,8 @@ A condition is composed of an operator and a version. The supported operators ar
- `1.0.0`, `=1.0.0`, `==1.0.0` Equal to `1.0.0`
- `!1.0.0`, `!=1.0.0` Not equal to `1.0.0`. Excludes version `1.0.0`.
Note that spaces between the operator and the version will be gracefully tolerated.
A `Range` can link multiple `Ranges` separated by space:
Ranges can be linked by logical AND:

17
vendor/github.com/blang/semver/package.json generated vendored Normal file
View file

@ -0,0 +1,17 @@
{
"author": "blang",
"bugs": {
"URL": "https://github.com/blang/semver/issues",
"url": "https://github.com/blang/semver/issues"
},
"gx": {
"dvcsimport": "github.com/blang/semver"
},
"gxVersion": "0.10.0",
"language": "go",
"license": "MIT",
"name": "semver",
"releaseCmd": "git commit -a -m \"gx publish $VERSION\"",
"version": "3.5.1"
}

View file

@ -2,10 +2,33 @@ package semver
import (
"fmt"
"strconv"
"strings"
"unicode"
)
type wildcardType int
const (
noneWildcard wildcardType = iota
majorWildcard wildcardType = 1
minorWildcard wildcardType = 2
patchWildcard wildcardType = 3
)
func wildcardTypefromInt(i int) wildcardType {
switch i {
case 1:
return majorWildcard
case 2:
return minorWildcard
case 3:
return patchWildcard
default:
return noneWildcard
}
}
type comparator func(Version, Version) bool
var (
@ -92,8 +115,12 @@ func ParseRange(s string) (Range, error) {
if err != nil {
return nil, err
}
expandedParts, err := expandWildcardVersion(orParts)
if err != nil {
return nil, err
}
var orFn Range
for _, p := range orParts {
for _, p := range expandedParts {
var andFn Range
for _, ap := range p {
opStr, vStr, err := splitComparatorVersion(ap)
@ -164,20 +191,39 @@ func buildVersionRange(opStr, vStr string) (*versionRange, error) {
}
// splitAndTrim splits a range string by spaces and cleans leading and trailing spaces
// inArray checks if a byte is contained in an array of bytes
func inArray(s byte, list []byte) bool {
for _, el := range list {
if el == s {
return true
}
}
return false
}
// splitAndTrim splits a range string by spaces and cleans whitespaces
func splitAndTrim(s string) (result []string) {
last := 0
var lastChar byte
excludeFromSplit := []byte{'>', '<', '='}
for i := 0; i < len(s); i++ {
if s[i] == ' ' {
if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) {
if last < i-1 {
result = append(result, s[last:i])
}
last = i + 1
} else if s[i] != ' ' {
lastChar = s[i]
}
}
if last < len(s)-1 {
result = append(result, s[last:])
}
for i, v := range result {
result[i] = strings.Replace(v, " ", "", -1)
}
// parts := strings.Split(s, " ")
// for _, x := range parts {
// if s := strings.TrimSpace(x); len(s) != 0 {
@ -188,7 +234,6 @@ func splitAndTrim(s string) (result []string) {
}
// splitComparatorVersion splits the comparator from the version.
// Spaces between the comparator and the version are not allowed.
// Input must be free of leading or trailing spaces.
func splitComparatorVersion(s string) (string, string, error) {
i := strings.IndexFunc(s, unicode.IsDigit)
@ -198,6 +243,144 @@ func splitComparatorVersion(s string) (string, string, error) {
return strings.TrimSpace(s[0:i]), s[i:], nil
}
// getWildcardType will return the type of wildcard that the
// passed version contains
func getWildcardType(vStr string) wildcardType {
parts := strings.Split(vStr, ".")
nparts := len(parts)
wildcard := parts[nparts-1]
possibleWildcardType := wildcardTypefromInt(nparts)
if wildcard == "x" {
return possibleWildcardType
}
return noneWildcard
}
// createVersionFromWildcard will convert a wildcard version
// into a regular version, replacing 'x's with '0's, handling
// special cases like '1.x.x' and '1.x'
func createVersionFromWildcard(vStr string) string {
// handle 1.x.x
vStr2 := strings.Replace(vStr, ".x.x", ".x", 1)
vStr2 = strings.Replace(vStr2, ".x", ".0", 1)
parts := strings.Split(vStr2, ".")
// handle 1.x
if len(parts) == 2 {
return vStr2 + ".0"
}
return vStr2
}
// incrementMajorVersion will increment the major version
// of the passed version
func incrementMajorVersion(vStr string) (string, error) {
parts := strings.Split(vStr, ".")
i, err := strconv.Atoi(parts[0])
if err != nil {
return "", err
}
parts[0] = strconv.Itoa(i + 1)
return strings.Join(parts, "."), nil
}
// incrementMajorVersion will increment the minor version
// of the passed version
func incrementMinorVersion(vStr string) (string, error) {
parts := strings.Split(vStr, ".")
i, err := strconv.Atoi(parts[1])
if err != nil {
return "", err
}
parts[1] = strconv.Itoa(i + 1)
return strings.Join(parts, "."), nil
}
// expandWildcardVersion will expand wildcards inside versions
// following these rules:
//
// * when dealing with patch wildcards:
// >= 1.2.x will become >= 1.2.0
// <= 1.2.x will become < 1.3.0
// > 1.2.x will become >= 1.3.0
// < 1.2.x will become < 1.2.0
// != 1.2.x will become < 1.2.0 >= 1.3.0
//
// * when dealing with minor wildcards:
// >= 1.x will become >= 1.0.0
// <= 1.x will become < 2.0.0
// > 1.x will become >= 2.0.0
// < 1.0 will become < 1.0.0
// != 1.x will become < 1.0.0 >= 2.0.0
//
// * when dealing with wildcards without
// version operator:
// 1.2.x will become >= 1.2.0 < 1.3.0
// 1.x will become >= 1.0.0 < 2.0.0
func expandWildcardVersion(parts [][]string) ([][]string, error) {
var expandedParts [][]string
for _, p := range parts {
var newParts []string
for _, ap := range p {
if strings.Index(ap, "x") != -1 {
opStr, vStr, err := splitComparatorVersion(ap)
if err != nil {
return nil, err
}
versionWildcardType := getWildcardType(vStr)
flatVersion := createVersionFromWildcard(vStr)
var resultOperator string
var shouldIncrementVersion bool
switch opStr {
case ">":
resultOperator = ">="
shouldIncrementVersion = true
case ">=":
resultOperator = ">="
case "<":
resultOperator = "<"
case "<=":
resultOperator = "<"
shouldIncrementVersion = true
case "", "=", "==":
newParts = append(newParts, ">="+flatVersion)
resultOperator = "<"
shouldIncrementVersion = true
case "!=", "!":
newParts = append(newParts, "<"+flatVersion)
resultOperator = ">="
shouldIncrementVersion = true
}
var resultVersion string
if shouldIncrementVersion {
switch versionWildcardType {
case patchWildcard:
resultVersion, _ = incrementMinorVersion(flatVersion)
case minorWildcard:
resultVersion, _ = incrementMajorVersion(flatVersion)
}
} else {
resultVersion = flatVersion
}
ap = resultOperator + resultVersion
}
newParts = append(newParts, ap)
}
expandedParts = append(expandedParts, newParts)
}
return expandedParts, nil
}
func parseComparator(s string) comparator {
switch s {
case "==":
@ -222,3 +405,12 @@ func parseComparator(s string) comparator {
return nil
}
// MustParseRange is like ParseRange but panics if the range cannot be parsed.
func MustParseRange(s string) Range {
r, err := ParseRange(s)
if err != nil {
panic(`semver: ParseRange(` + s + `): ` + err.Error())
}
return r
}

View file

@ -200,6 +200,29 @@ func Make(s string) (Version, error) {
return Parse(s)
}
// ParseTolerant allows for certain version specifications that do not strictly adhere to semver
// specs to be parsed by this library. It does so by normalizing versions before passing them to
// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions
// with only major and minor components specified
func ParseTolerant(s string) (Version, error) {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "v")
// Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3)
if len(parts) < 3 {
if strings.ContainsAny(parts[len(parts)-1], "+-") {
return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data")
}
for len(parts) < 3 {
parts = append(parts, "0")
}
s = strings.Join(parts, ".")
}
return Parse(s)
}
// Parse parses version string and returns a validated Version or error
func Parse(s string) (Version, error) {
if len(s) == 0 {

2
vendor/modules.txt vendored
View file

@ -34,7 +34,7 @@ github.com/VividCortex/ewma
github.com/acarl005/stripansi
# github.com/beorn7/perks v1.0.1
github.com/beorn7/perks/quantile
# github.com/blang/semver v3.1.0+incompatible
# github.com/blang/semver v3.5.1+incompatible
github.com/blang/semver
# github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37
github.com/buger/goterm