diff options
author | Jhon Honce <jhonce@redhat.com> | 2020-05-18 18:05:02 -0700 |
---|---|---|
committer | Jhon Honce <jhonce@redhat.com> | 2020-05-20 10:21:30 -0700 |
commit | f9c392f50a631a181bc2aa194b9c46504506d657 (patch) | |
tree | 226b022eb0d7aef35ad65dcaa10c6a2ec9f56e7c /pkg/api | |
parent | 09f8f14b4f7d09946d3d5cfc5460ec9923f7da59 (diff) | |
download | podman-f9c392f50a631a181bc2aa194b9c46504506d657.tar.gz podman-f9c392f50a631a181bc2aa194b9c46504506d657.tar.bz2 podman-f9c392f50a631a181bc2aa194b9c46504506d657.zip |
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>
Diffstat (limited to 'pkg/api')
-rw-r--r-- | pkg/api/handlers/compat/ping.go | 8 | ||||
-rw-r--r-- | pkg/api/handlers/compat/version.go | 42 | ||||
-rw-r--r-- | pkg/api/handlers/handler.go | 6 | ||||
-rw-r--r-- | pkg/api/handlers/utils/handler.go | 86 | ||||
-rw-r--r-- | pkg/api/handlers/utils/handler_test.go | 139 |
5 files changed, 250 insertions, 31 deletions
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) + } +} |