mirror of
https://github.com/containers/podman.git
synced 2025-08-06 03:19:52 +08:00
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:
@ -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
|
||||
|
139
pkg/api/handlers/utils/handler_test.go
Normal file
139
pkg/api/handlers/utils/handler_test.go
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user