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"`