From 9634e7eef77abbba2584b8e78daf9c76cfdcedd9 Mon Sep 17 00:00:00 2001
From: Jhon Honce <jhonce@redhat.com>
Date: Thu, 23 Jan 2020 16:13:47 -0700
Subject: [PATCH] Add query parameter converters for complex types

* Add converter for URL query parameters of type map[string][]string
* Add converter for URL query parameters of type time.Time
* Added function to allocate and configure schema.Decoder for API use
* Updated API handlers to leverage new converters, and correct handler
  code for filter type

An encoding example for a client using filters:

  v := map[string][]string{
      "dangling": {"true"},
  }
  payload, err := jsoniter.MarshalToString(v)
  if err != nil {
    panic(err)
  }
  payload = "?filters=" + url.QueryEscape(payload)

Signed-off-by: Jhon Honce <jhonce@redhat.com>
---
 pkg/api/handlers/decoder.go        | 50 ++++++++++++++++++++++++++++++
 pkg/api/handlers/events.go         | 25 ++++++---------
 pkg/api/handlers/generic/images.go | 17 +++++-----
 pkg/api/handlers/handler.go        |  9 +++---
 pkg/api/handlers/libpod/images.go  | 22 ++++++++-----
 pkg/api/handlers/libpod/pods.go    | 12 ++++---
 pkg/api/handlers/utils/images.go   | 11 ++++---
 pkg/api/server/server.go           |  7 ++---
 8 files changed, 104 insertions(+), 49 deletions(-)
 create mode 100644 pkg/api/handlers/decoder.go

diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go
new file mode 100644
index 0000000000..d87409394f
--- /dev/null
+++ b/pkg/api/handlers/decoder.go
@@ -0,0 +1,50 @@
+package handlers
+
+import (
+	"encoding/json"
+	"reflect"
+	"time"
+
+	"github.com/gorilla/schema"
+	"github.com/sirupsen/logrus"
+)
+
+// NewAPIDecoder returns a configured schema.Decoder
+func NewAPIDecoder() *schema.Decoder {
+	d := schema.NewDecoder()
+	d.IgnoreUnknownKeys(true)
+	d.RegisterConverter(map[string][]string{}, convertUrlValuesString)
+	d.RegisterConverter(time.Time{}, convertTimeString)
+	return d
+}
+
+// On client:
+// 	v := map[string][]string{
+//		"dangling": {"true"},
+//	}
+//
+//	payload, err := jsoniter.MarshalToString(v)
+//	if err != nil {
+//		panic(err)
+//	}
+//	payload = url.QueryEscape(payload)
+func convertUrlValuesString(query string) reflect.Value {
+	f := map[string][]string{}
+
+	err := json.Unmarshal([]byte(query), &f)
+	if err != nil {
+		logrus.Infof("convertUrlValuesString: Failed to Unmarshal %s: %s", query, err.Error())
+	}
+
+	return reflect.ValueOf(f)
+}
+
+func convertTimeString(query string) reflect.Value {
+	t, err := time.Parse(time.RFC3339, query)
+	if err != nil {
+		logrus.Infof("convertTimeString: Failed to Unmarshal %s: %s", query, err.Error())
+
+		return reflect.ValueOf(time.Time{})
+	}
+	return reflect.ValueOf(t)
+}
diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/events.go
index 900efa3da7..44bf352544 100644
--- a/pkg/api/handlers/events.go
+++ b/pkg/api/handlers/events.go
@@ -1,9 +1,10 @@
 package handlers
 
 import (
-	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
+	"time"
 
 	"github.com/containers/libpod/pkg/api/handlers/utils"
 	"github.com/pkg/errors"
@@ -11,30 +12,24 @@ import (
 
 func GetEvents(w http.ResponseWriter, r *http.Request) {
 	query := struct {
-		Since   string `json:"since"`
-		Until   string `json:"until"`
-		Filters string `json:"filters"`
+		Since   time.Time           `schema:"since"`
+		Until   time.Time           `schema:"until"`
+		Filters map[string][]string `schema:"filters"`
 	}{}
 	if err := decodeQuery(r, &query); err != nil {
 		utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
 	}
 
-	var filters = map[string][]string{}
-	if found := hasVar(r, "filters"); found {
-		if err := json.Unmarshal([]byte(query.Filters), &filters); err != nil {
-			utils.BadRequest(w, "filters", query.Filters, err)
-			return
+	var libpodFilters = []string{}
+	if _, found := r.URL.Query()["filters"]; found {
+		for k, v := range query.Filters {
+			libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0]))
 		}
 	}
 
-	var libpodFilters = make([]string, len(filters))
-	for k, v := range filters {
-		libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0]))
-	}
-
 	libpodEvents, err := getRuntime(r).GetEvents(libpodFilters)
 	if err != nil {
-		utils.BadRequest(w, "filters", query.Filters, err)
+		utils.BadRequest(w, "filters", strings.Join(r.URL.Query()["filters"], ", "), err)
 		return
 	}
 
diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/generic/images.go
index 395f64064a..93adb7f69d 100644
--- a/pkg/api/handlers/generic/images.go
+++ b/pkg/api/handlers/generic/images.go
@@ -62,14 +62,14 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
 	// 200 no error
 	// 500 internal
 	var (
-		dangling bool = true
+		dangling = true
 		err      error
 	)
 	decoder := r.Context().Value("decoder").(*schema.Decoder)
 	runtime := r.Context().Value("runtime").(*libpod.Runtime)
 
 	query := struct {
-		filters map[string]string
+		Filters map[string][]string `schema:"filters"`
 	}{
 		// This is where you can override the golang default value for one of fields
 	}
@@ -79,26 +79,25 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// FIXME This is likely wrong due to it not being a map[string][]string
-
 	// until ts is not supported on podman prune
-	if len(query.filters["until"]) > 0 {
-		utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "until is not supported yet"))
+	if v, found := query.Filters["until"]; found {
+		utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "until=%s is not supported yet", v))
 		return
 	}
 	// labels are not supported on podman prune
-	if len(query.filters["label"]) > 0 {
+	if _, found := query.Filters["since"]; found {
 		utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "labelis not supported yet"))
 		return
 	}
 
-	if len(query.filters["dangling"]) > 0 {
-		dangling, err = strconv.ParseBool(query.filters["dangling"])
+	if v, found := query.Filters["dangling"]; found {
+		dangling, err = strconv.ParseBool(v[0])
 		if err != nil {
 			utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "processing dangling filter"))
 			return
 		}
 	}
+
 	idr := []types.ImageDeleteResponseItem{}
 	//
 	// This code needs to be migrated to libpod to work correctly.  I could not
diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go
index 4f303f6ab1..d60a5b2393 100644
--- a/pkg/api/handlers/handler.go
+++ b/pkg/api/handlers/handler.go
@@ -15,10 +15,11 @@ func getVar(r *http.Request, k string) string {
 	return mux.Vars(r)[k]
 }
 
-func hasVar(r *http.Request, k string) bool {
-	_, found := mux.Vars(r)[k]
-	return found
-}
+// func hasVar(r *http.Request, k string) bool {
+// 	_, found := mux.Vars(r)[k]
+// 	return found
+// }
+
 func getName(r *http.Request) string {
 	return getVar(r, "name")
 }
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go
index 0d4e220a8c..bbc8c9346d 100644
--- a/pkg/api/handlers/libpod/images.go
+++ b/pkg/api/handlers/libpod/images.go
@@ -1,6 +1,7 @@
 package libpod
 
 import (
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"os"
@@ -42,12 +43,12 @@ func ImageExists(w http.ResponseWriter, r *http.Request) {
 func ImageTree(w http.ResponseWriter, r *http.Request) {
 	// tree is a bit of a mess ... logic is in adapter and therefore not callable from here. needs rework
 
-	//name := mux.Vars(r)["name"]
-	//_, layerInfoMap, _, err := s.Runtime.Tree(name)
-	//if err != nil {
+	// name := mux.Vars(r)["name"]
+	// _, layerInfoMap, _, err := s.Runtime.Tree(name)
+	// if err != nil {
 	//	Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to find image information for %q", name))
 	//	return
-	//}
+	// }
 	//	it is not clear to me how to deal with this given all the processing of the image
 	// is in main.  we need to discuss how that really should be and return something useful.
 	handlers.UnsupportedHandler(w, r)
@@ -95,8 +96,8 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
 	runtime := r.Context().Value("runtime").(*libpod.Runtime)
 	decoder := r.Context().Value("decoder").(*schema.Decoder)
 	query := struct {
-		All     bool     `schema:"all"`
-		Filters []string `schema:"filters"`
+		All     bool                `schema:"all"`
+		Filters map[string][]string `schema:"filters"`
 	}{
 		// override any golang type defaults
 	}
@@ -106,7 +107,14 @@ func PruneImages(w http.ResponseWriter, r *http.Request) {
 			errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
 		return
 	}
-	cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, query.Filters)
+
+	var libpodFilters = []string{}
+	if _, found := r.URL.Query()["filters"]; found {
+		for k, v := range query.Filters {
+			libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0]))
+		}
+	}
+	cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, libpodFilters)
 	if err != nil {
 		utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
 		return
diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go
index 14f8e8de74..656a75646b 100644
--- a/pkg/api/handlers/libpod/pods.go
+++ b/pkg/api/handlers/libpod/pods.go
@@ -108,7 +108,7 @@ func Pods(w http.ResponseWriter, r *http.Request) {
 	)
 	decoder := r.Context().Value("decoder").(*schema.Decoder)
 	query := struct {
-		filters []string `schema:"filters"`
+		Filters map[string][]string `schema:"filters"`
 	}{
 		// override any golang type defaults
 	}
@@ -117,10 +117,12 @@ func Pods(w http.ResponseWriter, r *http.Request) {
 			errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
 		return
 	}
-	if len(query.filters) > 0 {
+
+	if _, found := r.URL.Query()["filters"]; found {
 		utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented)
 		return
 	}
+
 	pods, err := runtime.GetAllPods()
 	if err != nil {
 		utils.Error(w, "Something went wrong", http.StatusInternalServerError, err)
@@ -164,7 +166,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) {
 		decoder   = r.Context().Value("decoder").(*schema.Decoder)
 	)
 	query := struct {
-		timeout int `schema:"t"`
+		Timeout int `schema:"t"`
 	}{
 		// override any golang type defaults
 	}
@@ -207,8 +209,8 @@ func PodStop(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if query.timeout > 0 {
-		_, stopError = pod.StopWithTimeout(r.Context(), false, query.timeout)
+	if query.Timeout > 0 {
+		_, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout)
 	} else {
 		_, stopError = pod.Stop(r.Context(), false)
 	}
diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go
index 9445298cae..a0d3404718 100644
--- a/pkg/api/handlers/utils/images.go
+++ b/pkg/api/handlers/utils/images.go
@@ -15,17 +15,18 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) {
 	decoder := r.Context().Value("decoder").(*schema.Decoder)
 	runtime := r.Context().Value("runtime").(*libpod.Runtime)
 	query := struct {
-		//all     bool # all is currently unused
-		filters []string
-		//digests bool # digests is currently unused
+		// all     bool # all is currently unused
+		Filters map[string][]string `schema:"filters"`
+		// digests bool # digests is currently unused
 	}{
 		// This is where you can override the golang default value for one of fields
 	}
 	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
 		return nil, err
 	}
-	filters := query.filters
-	if len(filters) < 1 {
+
+	var filters = []string{}
+	if _, found := r.URL.Query()["filters"]; found {
 		filters = append(filters, fmt.Sprintf("reference=%s", ""))
 	}
 	return runtime.ImageRuntime().GetImagesWithFilters(filters)
diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go
index 847d11c3c6..8c940763e4 100644
--- a/pkg/api/server/server.go
+++ b/pkg/api/server/server.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/containers/libpod/libpod"
+	"github.com/containers/libpod/pkg/api/handlers"
 	"github.com/coreos/go-systemd/activation"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/schema"
@@ -71,7 +72,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li
 			ReadTimeout:       20 * time.Second,
 			WriteTimeout:      2 * time.Minute,
 		},
-		Decoder:    schema.NewDecoder(),
+		Decoder:    handlers.NewAPIDecoder(),
 		Context:    nil,
 		Runtime:    runtime,
 		Listener:   *listener,
@@ -85,6 +86,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li
 	})
 
 	ctx, cancelFn := context.WithCancel(context.Background())
+	server.CancelFunc = cancelFn
 
 	// TODO: Use ConnContext when ported to go 1.13
 	ctx = context.WithValue(ctx, "decoder", server.Decoder)
@@ -92,9 +94,6 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li
 	ctx = context.WithValue(ctx, "shutdownFunc", server.Shutdown)
 	server.Context = ctx
 
-	server.CancelFunc = cancelFn
-	server.Decoder.IgnoreUnknownKeys(true)
-
 	router.NotFoundHandler = http.HandlerFunc(
 		func(w http.ResponseWriter, r *http.Request) {
 			// We can track user errors...