From 243f365aa4bea910c444ebd9d230ccf04ae54f30 Mon Sep 17 00:00:00 2001 From: Paul Holzinger <pholzing@redhat.com> Date: Thu, 24 Aug 2023 15:31:54 +0200 Subject: [PATCH] create apiutils package Move SupportedVersion() and IsLibpodRequest() to separate package to avoid import cycle when using it in libpod. Signed-off-by: Paul Holzinger <pholzing@redhat.com> --- pkg/api/handlers/libpod/manifests.go | 3 +- pkg/api/handlers/utils/apiutil/apiutil.go | 69 +++++++++ .../handlers/utils/apiutil/apiutil_test.go | 140 ++++++++++++++++++ pkg/api/handlers/utils/handler.go | 51 +------ pkg/api/handlers/utils/handler_test.go | 135 ----------------- 5 files changed, 214 insertions(+), 184 deletions(-) create mode 100644 pkg/api/handlers/utils/apiutil/apiutil.go create mode 100644 pkg/api/handlers/utils/apiutil/apiutil_test.go diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index 0434d5fbc0..b2e6272e58 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -17,6 +17,7 @@ import ( "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/pkg/api/handlers" "github.com/containers/podman/v4/pkg/api/handlers/utils" + "github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil" api "github.com/containers/podman/v4/pkg/api/types" "github.com/containers/podman/v4/pkg/auth" "github.com/containers/podman/v4/pkg/channel" @@ -80,7 +81,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { } status := http.StatusOK - if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == utils.ErrVersionNotSupported { + if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == apiutil.ErrVersionNotSupported { status = http.StatusCreated } diff --git a/pkg/api/handlers/utils/apiutil/apiutil.go b/pkg/api/handlers/utils/apiutil/apiutil.go new file mode 100644 index 0000000000..b33627e31e --- /dev/null +++ b/pkg/api/handlers/utils/apiutil/apiutil.go @@ -0,0 +1,69 @@ +package apiutil + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/blang/semver/v4" + "github.com/containers/podman/v4/version" + "github.com/gorilla/mux" +) + +var ( + // 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 { + split := strings.Split(r.URL.String(), "/") + 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, fmt.Errorf("unable to unescape given API version: %q: %w", val, err) + } + version, err = semver.ParseTolerant(safeVal) + if err != nil { + return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err) + } + + 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 := version.Compat + if IsLibpodRequest(r) { + tree = version.Libpod + } + + return SupportedVersion(r, + fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(), + version.APIVersion[tree][version.CurrentAPI].String())) +} diff --git a/pkg/api/handlers/utils/apiutil/apiutil_test.go b/pkg/api/handlers/utils/apiutil/apiutil_test.go new file mode 100644 index 0000000000..b86dc719cd --- /dev/null +++ b/pkg/api/handlers/utils/apiutil/apiutil_test.go @@ -0,0 +1,140 @@ +package apiutil + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containers/podman/v4/version" + "github.com/gorilla/mux" +) + +func TestSupportedVersion(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]), + nil) + if err != nil { + t.Fatal(err) + } + req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].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(http.MethodGet, + 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(http.MethodGet, + 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/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 1e5aa2c12d..5ebe3f7d37 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -1,79 +1,34 @@ package utils import ( - "errors" "fmt" "io" "net/http" "net/url" "os" - "strings" "unsafe" "github.com/blang/semver/v4" - "github.com/containers/podman/v4/version" "github.com/gorilla/mux" "github.com/gorilla/schema" jsoniter "github.com/json-iterator/go" "github.com/sirupsen/logrus" + "github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil" api "github.com/containers/podman/v4/pkg/api/types" ) -var ( - // 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 { - split := strings.Split(r.URL.String(), "/") - return len(split) >= 3 && split[2] == "libpod" + return apiutil.IsLibpodRequest(r) } // 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, fmt.Errorf("unable to unescape given API version: %q: %w", val, err) - } - version, err = semver.ParseTolerant(safeVal) - if err != nil { - return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err) - } - - 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 := version.Compat - if IsLibpodRequest(r) { - tree = version.Libpod - } - - return SupportedVersion(r, - fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(), - version.APIVersion[tree][version.CurrentAPI].String())) + return apiutil.SupportedVersion(r, condition) } // WriteResponse encodes the given value as JSON or string and renders it for http client diff --git a/pkg/api/handlers/utils/handler_test.go b/pkg/api/handlers/utils/handler_test.go index afa6f17eb1..099f4169b9 100644 --- a/pkg/api/handlers/utils/handler_test.go +++ b/pkg/api/handlers/utils/handler_test.go @@ -1,144 +1,9 @@ package utils import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" "testing" - - "github.com/containers/podman/v4/version" - "github.com/gorilla/mux" ) -func TestSupportedVersion(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, - fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]), - nil) - if err != nil { - t.Fatal(err) - } - req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].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(http.MethodGet, - 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(http.MethodGet, - 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) - } -} - func TestErrorEncoderFuncOmit(t *testing.T) { data, err := json.Marshal(struct { Err error `json:"err,omitempty"`