diff options
Diffstat (limited to 'pkg/api/handlers/utils')
-rw-r--r-- | pkg/api/handlers/utils/handler.go | 86 | ||||
-rw-r--r-- | pkg/api/handlers/utils/handler_test.go | 139 |
2 files changed, 225 insertions, 0 deletions
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) + } +} |