summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rwxr-xr-xhack/podman-registry231
-rw-r--r--hack/podman-registry-go/registry.go98
-rw-r--r--hack/podman-registry-go/registry_test.go40
-rw-r--r--pkg/api/handlers/compat/ping.go8
-rw-r--r--pkg/api/handlers/compat/version.go42
-rw-r--r--pkg/api/handlers/handler.go6
-rw-r--r--pkg/api/handlers/utils/handler.go86
-rw-r--r--pkg/api/handlers/utils/handler_test.go139
-rw-r--r--pkg/api/server/register_images.go2
-rw-r--r--pkg/bindings/bindings.go7
-rw-r--r--pkg/bindings/connection.go17
-rw-r--r--pkg/bindings/version.go3
-rw-r--r--test/apiv2/01-basic.at14
-rw-r--r--vendor/github.com/blang/semver/.travis.yml21
-rw-r--r--vendor/github.com/blang/semver/README.md5
-rw-r--r--vendor/github.com/blang/semver/package.json17
-rw-r--r--vendor/github.com/blang/semver/range.go200
-rw-r--r--vendor/github.com/blang/semver/semver.go23
-rw-r--r--vendor/modules.txt2
22 files changed, 918 insertions, 54 deletions
diff --git a/README.md b/README.md
index a8e0e65ef..4a5fb2333 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,12 @@ Libpod provides a library for applications looking to use the Container Pod conc
popularized by Kubernetes. Libpod also contains the Pod Manager tool `(Podman)`. Podman manages pods, containers, container images, and container volumes.
* [Latest Version: 1.9.2](https://github.com/containers/libpod/releases/latest)
+ * Latest Remote client for Windows
+ * Latest Remote client for MacOs
+ * Latest Static Remote client for Linux
+
* [Continuous Integration:](contrib/cirrus/README.md) [![Build Status](https://api.cirrus-ci.com/github/containers/libpod.svg)](https://cirrus-ci.com/github/containers/libpod/master)
* [GoDoc: ![GoDoc](https://godoc.org/github.com/containers/libpod/libpod?status.svg)](https://godoc.org/github.com/containers/libpod/libpod)
-* Automated continuous release downloads (including remote-client):
- * [Latest remote client for Windows](https://storage.googleapis.com/libpod-master-releases/podman-remote-latest-master-windows-amd64.msi)
- * [Latest remote client for MacOS](https://storage.googleapis.com/libpod-master-releases/podman-remote-latest-master-darwin-amd64.zip)
- * [Latest Snap package](https://snapcraft.io/podman)
## Overview and scope
diff --git a/go.mod b/go.mod
index aac3f4c6f..3160186aa 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 9a89453e1..e5d967b7d 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/hack/podman-registry b/hack/podman-registry
new file mode 100755
index 000000000..e7708ce6a
--- /dev/null
+++ b/hack/podman-registry
@@ -0,0 +1,231 @@
+#! /bin/bash
+#
+# podman-registry - start/stop/monitor a local instance of registry:2
+#
+ME=$(basename $0)
+
+###############################################################################
+# BEGIN defaults
+
+PODMAN_REGISTRY_IMAGE=docker.io/library/registry:2
+
+PODMAN_REGISTRY_USER=
+PODMAN_REGISTRY_PASS=
+PODMAN_REGISTRY_PORT=
+
+# Podman binary to run
+PODMAN=${PODMAN:-$(type -p podman)}
+
+# END defaults
+###############################################################################
+# BEGIN help messages
+
+missing=" argument is missing; see $ME --help for details"
+usage="Usage: $ME [options] [start|stop|ps|logs]
+
+$ME manages a local instance of a container registry.
+
+When called to start a registry, $ME will pull an image
+into a local temporary directory, create an htpasswd, start the
+registry, and dump a series of environment variables to stdout:
+
+ \$ $ME start
+ PODMAN_REGISTRY_IMAGE=\"docker.io/library/registry:2\"
+ PODMAN_REGISTRY_PORT=\"5050\"
+ PODMAN_REGISTRY_USER=\"userZ3RZ\"
+ PODMAN_REGISTRY_PASS=\"T8JVJzKrcl4p6uT\"
+
+Expected usage, therefore, is something like this in a script
+
+ eval \$($ME start)
+
+To stop the registry, you will need to know the port number:
+
+ $ME -P \$PODMAN_REGISTRY_PORT stop
+
+Override the default image, port, user, password with:
+
+ -i IMAGE registry image to pull (default: $PODMAN_REGISTRY_IMAGE)
+ -u USER registry user (default: random)
+ -p PASS password for registry user (default: random)
+ -P PORT port to bind to (on 127.0.0.1) (default: random, 5000-5999)
+
+Other options:
+
+ -h display usage message
+"
+
+die () {
+ echo "$ME: $*" >&2
+ exit 1
+}
+
+# END help messages
+###############################################################################
+# BEGIN option processing
+
+while getopts "i:u:p:P:hv" opt; do
+ case "$opt" in
+ i) PODMAN_REGISTRY_IMAGE=$OPTARG ;;
+ u) PODMAN_REGISTRY_USER=$OPTARG ;;
+ p) PODMAN_REGISTRY_PASS=$OPTARG ;;
+ P) PODMAN_REGISTRY_PORT=$OPTARG ;;
+ h) echo "$usage"; exit 0;;
+ v) verbose=1 ;;
+ \?) echo "Run '$ME -h' for help" >&2; exit 1;;
+ esac
+done
+shift $((OPTIND-1))
+
+# END option processing
+###############################################################################
+# BEGIN helper functions
+
+function random_string() {
+ local length=${1:-10}
+
+ head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length
+}
+
+function podman() {
+ if [ -z "${PODMAN_REGISTRY_PORT}" ]; then
+ die "podman port undefined; please invoke me with -P PORT"
+ fi
+
+ if [ -z "${PODMAN_REGISTRY_WORKDIR}" ]; then
+ PODMAN_REGISTRY_WORKDIR=${TMPDIR:-/tmp}/podman-registry-${PODMAN_REGISTRY_PORT}
+ if [ ! -d ${PODMAN_REGISTRY_WORKDIR} ]; then
+ die "$ME: directory does not exist: ${PODMAN_REGISTRY_WORKDIR}"
+ fi
+ fi
+
+ ${PODMAN} --root ${PODMAN_REGISTRY_WORKDIR}/root \
+ --runroot ${PODMAN_REGISTRY_WORKDIR}/runroot \
+ "$@"
+}
+
+# END helper functions
+###############################################################################
+# BEGIN action processing
+
+function do_start() {
+ # If called without a port, assign a random one in the 5xxx range
+ if [ -z "${PODMAN_REGISTRY_PORT}" ]; then
+ for port in $(shuf -i 5000-5999);do
+ if ! { exec 3<> /dev/tcp/127.0.0.1/$port; } &>/dev/null; then
+ PODMAN_REGISTRY_PORT=$port
+ break
+ fi
+ done
+ fi
+
+ PODMAN_REGISTRY_WORKDIR=${TMPDIR:-/tmp}/podman-registry-${PODMAN_REGISTRY_PORT}
+ if [ -d ${PODMAN_REGISTRY_WORKDIR} ]; then
+ die "$ME: directory exists: ${PODMAN_REGISTRY_WORKDIR} (another registry might already be running on this port)"
+ fi
+
+ # Randomly-generated username and password, if none given on command line
+ if [ -z "${PODMAN_REGISTRY_USER}" ]; then
+ PODMAN_REGISTRY_USER="user$(random_string 4)"
+ fi
+ if [ -z "${PODMAN_REGISTRY_PASS}" ]; then
+ PODMAN_REGISTRY_PASS=$(random_string 15)
+ fi
+
+ # Die on any error
+ set -e
+
+ mkdir -p ${PODMAN_REGISTRY_WORKDIR}
+
+ local AUTHDIR=${PODMAN_REGISTRY_WORKDIR}/auth
+ mkdir -p $AUTHDIR
+
+ # We have to be silent; our only output must be env. vars. Log output here.
+ local log=${PODMAN_REGISTRY_WORKDIR}/log
+ touch $log
+
+ # Pull registry image, but into a separate container storage
+ mkdir -p ${PODMAN_REGISTRY_WORKDIR}/root
+ mkdir -p ${PODMAN_REGISTRY_WORKDIR}/runroot
+
+ # Give it three tries, to compensate for flakes
+ podman pull ${PODMAN_REGISTRY_IMAGE} &>> $log ||
+ podman pull ${PODMAN_REGISTRY_IMAGE} &>> $log ||
+ podman pull ${PODMAN_REGISTRY_IMAGE} &>> $log
+
+ # Registry image needs a cert. Self-signed is good enough.
+ local CERT=$AUTHDIR/domain.crt
+ # FIXME: if this fails, we fail silently! It'd be more helpful
+ # to say 'openssl failed' and cat the logfile
+ openssl req -newkey rsa:4096 -nodes -sha256 \
+ -keyout ${AUTHDIR}/domain.key -x509 -days 2 \
+ -out ${AUTHDIR}/domain.crt \
+ -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=localhost" \
+ &>> $log
+
+ # Store credentials where container will see them
+ podman run --rm \
+ --entrypoint htpasswd ${PODMAN_REGISTRY_IMAGE} \
+ -Bbn ${PODMAN_REGISTRY_USER} ${PODMAN_REGISTRY_PASS} \
+ > $AUTHDIR/htpasswd
+
+ # In case someone needs to debug
+ echo "${PODMAN_REGISTRY_USER}:${PODMAN_REGISTRY_PASS}" \
+ > $AUTHDIR/htpasswd-plaintext
+
+ # Run the registry container.
+ podman run --quiet -d \
+ -p ${PODMAN_REGISTRY_PORT}:5000 \
+ --name registry \
+ -v $AUTHDIR:/auth:Z \
+ -e "REGISTRY_AUTH=htpasswd" \
+ -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
+ -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
+ -e "REGISTRY_HTTP_TLS_CERTIFICATE=/auth/domain.crt" \
+ -e "REGISTRY_HTTP_TLS_KEY=/auth/domain.key" \
+ registry:2 &>> $log
+
+ # Dump settings. Our caller will use these to access the registry.
+ for v in IMAGE PORT USER PASS; do
+ echo "PODMAN_REGISTRY_${v}=\"$(eval echo \$PODMAN_REGISTRY_${v})\""
+ done
+}
+
+
+function do_stop() {
+ podman stop registry
+ podman rm -f registry
+
+ rm -rf ${PODMAN_REGISTRY_WORKDIR}
+}
+
+
+function do_ps() {
+ podman ps -a
+}
+
+
+function do_logs() {
+ podman logs registry
+}
+
+# END action processing
+###############################################################################
+# BEGIN command-line processing
+
+# First command-line arg must be an action
+action=${1?ACTION$missing}
+shift
+
+case "$action" in
+ start) do_start ;;
+ stop) do_stop ;;
+ ps) do_ps ;;
+ logs) do_logs ;;
+ *) die "Unknown action '$action'; must be start / stop / ps / logs" ;;
+esac
+
+# END command-line processing
+###############################################################################
+
+exit 0
diff --git a/hack/podman-registry-go/registry.go b/hack/podman-registry-go/registry.go
new file mode 100644
index 000000000..a83304914
--- /dev/null
+++ b/hack/podman-registry-go/registry.go
@@ -0,0 +1,98 @@
+package registry
+
+import (
+ "strings"
+
+ "github.com/containers/libpod/utils"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+const (
+ imageKey = "PODMAN_REGISTRY_IMAGE"
+ userKey = "PODMAN_REGISTRY_USER"
+ passKey = "PODMAN_REGISTRY_PASS"
+ portKey = "PODMAN_REGISTRY_PORT"
+)
+
+var binary = "podman-registry"
+
+// Registry is locally running registry.
+type Registry struct {
+ // Image - container image of the registry.
+ Image string
+ // User - the user to authenticate against the registry.
+ User string
+ // Password - the accompanying password for the user.
+ Password string
+ // Port - the port the registry is listening to on the host.
+ Port string
+ // running indicates if the registry is running.
+ running bool
+}
+
+// Start a new registry and return it along with it's image, user, password, and port.
+func Start() (*Registry, error) {
+ // Start a registry.
+ out, err := utils.ExecCmd(binary, "start")
+ if err != nil {
+ return nil, errors.Wrapf(err, "error running %q: %s", binary, out)
+ }
+
+ // Parse the output.
+ registry := Registry{}
+ for _, s := range strings.Split(out, "\n") {
+ if s == "" {
+ continue
+ }
+ spl := strings.Split(s, "=")
+ if len(spl) != 2 {
+ return nil, errors.Errorf("unexpected output format %q: want 'PODMAN_...=...'", s)
+ }
+ key := spl[0]
+ val := strings.TrimSuffix(strings.TrimPrefix(spl[1], "\""), "\"")
+ switch key {
+ case imageKey:
+ registry.Image = val
+ case userKey:
+ registry.User = val
+ case passKey:
+ registry.Password = val
+ case portKey:
+ registry.Port = val
+ default:
+ logrus.Errorf("unexpected podman-registry output: %q", s)
+ }
+ }
+
+ // Extra sanity check.
+ if registry.Image == "" {
+ return nil, errors.Errorf("unexpected output %q: %q missing", out, imageKey)
+ }
+ if registry.User == "" {
+ return nil, errors.Errorf("unexpected output %q: %q missing", out, userKey)
+ }
+ if registry.Password == "" {
+ return nil, errors.Errorf("unexpected output %q: %q missing", out, passKey)
+ }
+ if registry.Port == "" {
+ return nil, errors.Errorf("unexpected output %q: %q missing", out, portKey)
+ }
+
+ registry.running = true
+
+ return &registry, nil
+}
+
+// Stop the registry.
+func (r *Registry) Stop() error {
+ // Stop a registry.
+ if !r.running {
+ return nil
+ }
+ if _, err := utils.ExecCmd(binary, "-P", r.Port, "stop"); err != nil {
+ return errors.Wrapf(err, "error stopping registry (%v) with %q", *r, binary)
+ }
+ r.running = false
+ return nil
+}
diff --git a/hack/podman-registry-go/registry_test.go b/hack/podman-registry-go/registry_test.go
new file mode 100644
index 000000000..4e4bf5fe2
--- /dev/null
+++ b/hack/podman-registry-go/registry_test.go
@@ -0,0 +1,40 @@
+package registry
+
+import (
+ "testing"
+
+ "github.com/hashicorp/go-multierror"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestStartAndStopMultipleRegistries(t *testing.T) {
+ binary = "../podman-registry"
+
+ registries := []*Registry{}
+
+ // Start registries.
+ var errors *multierror.Error
+ for i := 0; i < 3; i++ {
+ reg, err := Start()
+ if err != nil {
+ errors = multierror.Append(errors, err)
+ continue
+ }
+ assert.True(t, len(reg.Image) > 0)
+ assert.True(t, len(reg.User) > 0)
+ assert.True(t, len(reg.Password) > 0)
+ assert.True(t, len(reg.Port) > 0)
+ registries = append(registries, reg)
+ }
+
+ // Stop registries.
+ for _, reg := range registries {
+ // Make sure we can stop it properly.
+ errors = multierror.Append(errors, reg.Stop())
+ // Stopping an already stopped registry is fine as well.
+ errors = multierror.Append(errors, reg.Stop())
+ }
+
+ require.NoError(t, errors.ErrorOrNil())
+}
diff --git a/pkg/api/handlers/compat/ping.go b/pkg/api/handlers/compat/ping.go
index 6e77e270f..abee3d8e8 100644
--- a/pkg/api/handlers/compat/ping.go
+++ b/pkg/api/handlers/compat/ping.go
@@ -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)
diff --git a/pkg/api/handlers/compat/version.go b/pkg/api/handlers/compat/version.go
index 8786f1d5b..bfc226bb8 100644
--- a/pkg/api/handlers/compat/version.go
+++ b/pkg/api/handlers/compat/version.go
@@ -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,
+ }})
}
diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go
deleted file mode 100644
index 2dd2c886b..000000000
--- a/pkg/api/handlers/handler.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package handlers
-
-const (
- DefaultApiVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/
- MinimalApiVersion = "1.24"
-)
diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go
index b5bd488fb..2f4a54b98 100644
--- a/pkg/api/handlers/utils/handler.go
+++ b/pkg/api/handlers/utils/handler.go
@@ -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
diff --git a/pkg/api/handlers/utils/handler_test.go b/pkg/api/handlers/utils/handler_test.go
new file mode 100644
index 000000000..6009432b5
--- /dev/null
+++ b/pkg/api/handlers/utils/handler_test.go
@@ -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)
+ }
+}
diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go
index 36f939779..01854b9c4 100644
--- a/pkg/api/server/register_images.go
+++ b/pkg/api/server/register_images.go
@@ -698,7 +698,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// responses:
// 200:
// $ref: '#/responses/LibpodImageTreeResponse'
- // 401:
+ // 404:
// $ref: '#/responses/NoSuchImage'
// 500:
// $ref: '#/responses/InternalError'
diff --git a/pkg/bindings/bindings.go b/pkg/bindings/bindings.go
index 5e2882aae..7e2a444bd 100644
--- a/pkg/bindings/bindings.go
+++ b/pkg/bindings/bindings.go
@@ -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")
)
diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go
index d83c0482c..d21d55beb 100644
--- a/pkg/bindings/connection.go
+++ b/pkg/bindings/connection.go
@@ -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)
}
diff --git a/pkg/bindings/version.go b/pkg/bindings/version.go
deleted file mode 100644
index c833a644c..000000000
--- a/pkg/bindings/version.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package bindings
-
-func (c Connection) Version() {}
diff --git a/test/apiv2/01-basic.at b/test/apiv2/01-basic.at
index 0e94ddb7a..18ec9bbe8 100644
--- a/test/apiv2/01-basic.at
+++ b/test/apiv2/01-basic.at
@@ -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
diff --git a/vendor/github.com/blang/semver/.travis.yml b/vendor/github.com/blang/semver/.travis.yml
new file mode 100644
index 000000000..102fb9a69
--- /dev/null
+++ b/vendor/github.com/blang/semver/.travis.yml
@@ -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=
diff --git a/vendor/github.com/blang/semver/README.md b/vendor/github.com/blang/semver/README.md
index 4399639e2..08b2e4a3d 100644
--- a/vendor/github.com/blang/semver/README.md
+++ b/vendor/github.com/blang/semver/README.md
@@ -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:
diff --git a/vendor/github.com/blang/semver/package.json b/vendor/github.com/blang/semver/package.json
new file mode 100644
index 000000000..1cf8ebdd9
--- /dev/null
+++ b/vendor/github.com/blang/semver/package.json
@@ -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"
+}
+
diff --git a/vendor/github.com/blang/semver/range.go b/vendor/github.com/blang/semver/range.go
index 0a8eaa1c9..fca406d47 100644
--- a/vendor/github.com/blang/semver/range.go
+++ b/vendor/github.com/blang/semver/range.go
@@ -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
+}
diff --git a/vendor/github.com/blang/semver/semver.go b/vendor/github.com/blang/semver/semver.go
index bbf85ce97..8ee0842e6 100644
--- a/vendor/github.com/blang/semver/semver.go
+++ b/vendor/github.com/blang/semver/semver.go
@@ -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 {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index f3d216559..9159143c4 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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