Skip to content

Commit 6d36c03

Browse files
authored
fix: Refactor filters parsing (runfinch#181)
fix:Rrefactor filters parsing Signed-off-by: Arjun Raja Yogidas <[email protected]>
1 parent 08878dc commit 6d36c03

File tree

6 files changed

+211
-34
lines changed

6 files changed

+211
-34
lines changed

api/handlers/container/list.go

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package container
55

66
import (
7-
"encoding/json"
87
"fmt"
98
"net/http"
109
"net/url"
@@ -14,6 +13,7 @@ import (
1413
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
1514

1615
"github.com/runfinch/finch-daemon/api/response"
16+
"github.com/runfinch/finch-daemon/api/types"
1717
)
1818

1919
const (
@@ -45,7 +45,7 @@ func (h *handler) list(w http.ResponseWriter, r *http.Request) {
4545
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg(fmt.Sprintf("invalid query parameter \"size\": %s", err)))
4646
return
4747
}
48-
filters, err := parseFiltersQP(q)
48+
filters, err := NerdctlFiltersFromAPIFilters(q)
4949
if err != nil {
5050
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg(fmt.Sprintf("invalid query parameter \"filters\": %s", err)))
5151
return
@@ -59,7 +59,7 @@ func (h *handler) list(w http.ResponseWriter, r *http.Request) {
5959
LastN: limit,
6060
Truncate: true,
6161
Size: size,
62-
Filters: nerdctlFiltersFromAPIFilters(filters),
62+
Filters: filters,
6363
}
6464
containers, err := h.service.List(ctx, listOpts)
6565
if err != nil {
@@ -69,16 +69,6 @@ func (h *handler) list(w http.ResponseWriter, r *http.Request) {
6969
response.JSON(w, http.StatusOK, containers)
7070
}
7171

72-
func nerdctlFiltersFromAPIFilters(filters map[string][]string) []string {
73-
var ncFilters []string
74-
for filterType, filterList := range filters {
75-
for _, f := range filterList {
76-
ncFilters = append(ncFilters, fmt.Sprintf("%s=%s", filterType, f))
77-
}
78-
}
79-
return ncFilters
80-
}
81-
8272
func parseBoolQP(q url.Values, key string, defaultV bool) (bool, error) {
8373
v := q.Get(key)
8474
if v == "" {
@@ -105,14 +95,17 @@ func parseIntQP(q url.Values, key string, defaultV int) (int, error) {
10595
}
10696
}
10797

108-
func parseFiltersQP(q url.Values) (map[string][]string, error) {
109-
filters := make(map[string][]string)
110-
filterQuery := q.Get(filtersKey)
111-
if filterQuery != "" {
112-
err := json.Unmarshal([]byte(filterQuery), &filters)
113-
if err != nil {
114-
return nil, err
98+
func NerdctlFiltersFromAPIFilters(query url.Values) ([]string, error) {
99+
filters, err := types.ParseFilterArgs(query)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
var ncFilters []string
105+
for filterType, filterList := range filters.ToLegacyFormat() {
106+
for _, f := range filterList {
107+
ncFilters = append(ncFilters, fmt.Sprintf("%s=%s", filterType, f))
115108
}
116109
}
117-
return filters, nil
110+
return ncFilters, nil
118111
}

api/handlers/container/list_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ var _ = Describe("Container List API", func() {
5858
It("should return containers and 200 status code upon success with all query parameters", func() {
5959
req, err := http.NewRequest(http.MethodGet, "/containers/json?all=true&limit=1&size=true&filters={\"status\": [\"paused\"]}", nil)
6060
Expect(err).Should(BeNil())
61+
6162
listOpts := ncTypes.ContainerListOptions{
6263
GOptions: globalOpts,
6364
All: true,
@@ -119,7 +120,7 @@ var _ = Describe("Container List API", func() {
119120
It("should return 400 status code when there is error parsing filters", func() {
120121
req, err := http.NewRequest(http.MethodGet, "/containers/json?filters=invalid", nil)
121122
Expect(err).Should(BeNil())
122-
errorMsg := fmt.Sprintf("invalid query parameter \\\"filters\\\": %s", fmt.Errorf("invalid character 'i' looking for beginning of value"))
123+
errorMsg := fmt.Sprintf("invalid query parameter \\\"filters\\\": %s", fmt.Errorf("error parsing filters: invalid character 'i' looking for beginning of value"))
123124

124125
h.list(rr, req)
125126
Expect(rr.Body).Should(MatchJSON(`{"message": "` + errorMsg + `"}`))

api/handlers/system/events.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@ import (
1010

1111
eventtype "github.com/runfinch/finch-daemon/api/events"
1212
"github.com/runfinch/finch-daemon/api/response"
13+
"github.com/runfinch/finch-daemon/api/types"
1314
)
1415

1516
func (h *handler) events(w http.ResponseWriter, r *http.Request) {
16-
filters := make(map[string][]string)
17-
filterQuery := r.URL.Query().Get("filters")
18-
if filterQuery != "" {
19-
err := json.Unmarshal([]byte(filterQuery), &filters)
20-
if err != nil {
21-
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg(fmt.Sprintf("invalid filter: %s", err)))
22-
return
23-
}
17+
filters, err := types.ParseFilterArgs(r.URL.Query())
18+
if err != nil {
19+
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg(fmt.Sprintf("invalid filter: %s", err)))
20+
return
2421
}
2522

26-
eventCh, errCh := h.service.SubscribeEvents(r.Context(), filters)
23+
eventCh, errCh := h.service.SubscribeEvents(r.Context(), filters.ToLegacyFormat())
2724

2825
encoder := json.NewEncoder(w)
2926

api/handlers/system/events_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net/http"
1010
"net/http/httptest"
11+
"net/url"
1112
"sync"
1213
"time"
1314

@@ -56,6 +57,7 @@ var _ = Describe("Events API", func() {
5657
}
5758
mockEventJson, _ = json.Marshal(mockEvent)
5859
})
60+
5961
It("should return 200 and stream events on success", func() {
6062
// in order to add events to the channels while the events handler is running, we need to either run the handler
6163
// or the channel publisher in a separate goroutine as the events handler will block until it returns. because all
@@ -104,14 +106,15 @@ var _ = Describe("Events API", func() {
104106
Expect(err).Should(BeNil())
105107
Expect(line).Should(MatchJSON(mockEventJson))
106108
})
109+
107110
It("should return 400 if filters are not in the correct format", func() {
108111
req, _ := http.NewRequest(http.MethodGet, "/events?filters=bad", nil)
109-
req = mux.SetURLVars(req, map[string]string{"filters": "bad"})
110112

111113
h.events(rr, req)
112114
Expect(rr).Should(HaveHTTPStatus(http.StatusBadRequest))
113-
Expect(rr.Body).Should(MatchJSON(`{"message": "invalid filter: invalid character 'b' looking for beginning of value"}`))
115+
Expect(rr.Body).Should(MatchJSON(`{"message": "invalid filter: error parsing filters: invalid character 'b' looking for beginning of value"}`))
114116
})
117+
115118
It("should forward filters to the service", func() {
116119
var waitGroup sync.WaitGroup
117120

@@ -138,6 +141,7 @@ var _ = Describe("Events API", func() {
138141

139142
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
140143
})
144+
141145
It("should stop streaming if an error is received", func() {
142146
var waitGroup sync.WaitGroup
143147

@@ -165,4 +169,40 @@ var _ = Describe("Events API", func() {
165169
close(mockEventCh)
166170
close(mockErrCh)
167171
})
172+
173+
It("should handle multiple filters", func() {
174+
var waitGroup sync.WaitGroup
175+
mockEventCh = make(chan *events.Event)
176+
mockErrCh = make(chan error)
177+
178+
filters := map[string]map[string]bool{
179+
"type": {"container": true},
180+
"status": {"start": true, "stop": true},
181+
}
182+
filtersJSON, err := json.Marshal(filters)
183+
Expect(err).Should(BeNil())
184+
185+
url := fmt.Sprintf("/events?filters=%s", url.QueryEscape(string(filtersJSON)))
186+
req, err := http.NewRequest(http.MethodGet, url, nil)
187+
Expect(err).Should(BeNil())
188+
189+
s.EXPECT().SubscribeEvents(req.Context(), map[string][]string{
190+
"type": {"container"},
191+
"status": {"start", "stop"},
192+
}).Return(mockEventCh, mockErrCh)
193+
logger.EXPECT().Debugf("received error, exiting: %s", gomock.Any())
194+
195+
waitGroup.Add(1)
196+
go func() {
197+
defer waitGroup.Done()
198+
h.events(rr, req)
199+
}()
200+
201+
close(mockEventCh)
202+
close(mockErrCh)
203+
204+
waitGroup.Wait()
205+
206+
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
207+
})
168208
})

api/types/filter_types.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package types
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/url"
10+
)
11+
12+
// Filters represents a collection of filter types and their values.
13+
type Filters struct {
14+
filterSet map[string]map[string]bool
15+
}
16+
17+
// FilterKeyVal represents a single key-value pair for a filter.
18+
type FilterKeyVal struct {
19+
Key string
20+
Value string
21+
}
22+
23+
// newFilters creates a new Filters instance with the provided filter key-value pairs.
24+
func newFilters(filtersList ...FilterKeyVal) Filters {
25+
filters := Filters{filterSet: map[string]map[string]bool{}}
26+
for _, arg := range filtersList {
27+
filters.add(arg.Key, arg.Value)
28+
}
29+
return filters
30+
}
31+
32+
// getFiltersKeys returns all values for a given filter key.
33+
func (filters Filters) getFiltersKeys(key string) []string {
34+
values := filters.filterSet[key]
35+
if values == nil {
36+
return make([]string, 0)
37+
}
38+
slice := make([]string, 0, len(values))
39+
for key := range values {
40+
slice = append(slice, key)
41+
}
42+
return slice
43+
}
44+
45+
// add inserts a new key-value pair into the filter set.
46+
func (filters Filters) add(key, value string) {
47+
if _, ok := filters.filterSet[key]; ok {
48+
filters.filterSet[key][value] = true
49+
} else {
50+
filters.filterSet[key] = map[string]bool{value: true}
51+
}
52+
}
53+
54+
// getFilterFromLegacy converts the legacy filter format (map[string][]string)
55+
// to the new filter format (map[string]map[string]bool),
56+
// legacy filter format is currently the default format in the API spec.
57+
func getFilterFromLegacy(d map[string][]string) map[string]map[string]bool {
58+
m := map[string]map[string]bool{}
59+
for k, v := range d {
60+
values := map[string]bool{}
61+
for _, vv := range v {
62+
values[vv] = true
63+
}
64+
m[k] = values
65+
}
66+
return m
67+
}
68+
69+
// getFilterJSON parses a JSON string into a Filters struct.
70+
func getFilterJSON(p string) (Filters, error) {
71+
filters := newFilters()
72+
73+
if p == "" {
74+
return filters, nil
75+
}
76+
77+
raw := []byte(p)
78+
err := json.Unmarshal(raw, &filters)
79+
if err == nil {
80+
return filters, nil
81+
}
82+
83+
// Fallback to parsing arguments in the legacy slice format
84+
deprecated := map[string][]string{}
85+
if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil {
86+
return filters, legacyErr
87+
}
88+
89+
filters.filterSet = getFilterFromLegacy(deprecated)
90+
return filters, nil
91+
}
92+
93+
// UnmarshalJSON implements the json.Unmarshaler interface for Filters.
94+
func (filters Filters) UnmarshalJSON(raw []byte) error {
95+
return json.Unmarshal(raw, &filters.filterSet)
96+
}
97+
98+
// ToLegacyFormat converts the Filters struct to the legacy format (map[string][]string).
99+
func (filters Filters) ToLegacyFormat() map[string][]string {
100+
result := make(map[string][]string)
101+
for key := range filters.filterSet {
102+
values := filters.getFiltersKeys(key)
103+
if len(values) > 0 {
104+
result[key] = values
105+
}
106+
}
107+
return result
108+
}
109+
110+
// ParseFilterArgs extracts and parses filters from URL query parameters.
111+
func ParseFilterArgs(query url.Values) (Filters, error) {
112+
filterQuery := query.Get("filters")
113+
if filterQuery == "" {
114+
return newFilters(), nil
115+
}
116+
117+
filters, err := getFilterJSON(filterQuery)
118+
if err != nil {
119+
return Filters{}, fmt.Errorf("error parsing filters: %v", err)
120+
}
121+
122+
return filters, nil
123+
}

e2e/tests/container_list.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,32 @@ func ContainerList(opt *option.Option) {
226226
body, err := io.ReadAll(res.Body)
227227
Expect(err).Should(BeNil())
228228
defer res.Body.Close()
229-
errorMsg := fmt.Sprintf("invalid query parameter \\\"filters\\\": %s", fmt.Errorf("invalid character 'i' looking for beginning of value"))
229+
errorMsg := fmt.Sprintf("invalid query parameter \\\"filters\\\": %s", fmt.Errorf("error parsing filters: invalid character 'i' looking for beginning of value"))
230230
Expect(body).Should(MatchJSON(`{"message": "` + errorMsg + `"}`))
231231
})
232+
It("should list the running containers with new filter format", func() {
233+
id := command.StdoutStr(opt, "run", "-d", "--name", testContainerName, "--label", "com.example.foo=bar", defaultImage, "sleep", "infinity")
234+
want := []types.ContainerListItem{
235+
{
236+
Id: id[:12],
237+
Names: []string{wantContainerName},
238+
},
239+
}
240+
241+
// Using the new filter format
242+
newFormatFilter := `{"label":{"com.example.foo=bar":true}}`
243+
res, err := uClient.Get(client.ConvertToFinchUrl(version, "/containers/json?all=true&filters="+newFormatFilter))
244+
Expect(err).Should(BeNil())
245+
Expect(res.StatusCode).Should(Equal(http.StatusOK))
246+
247+
var got []types.ContainerListItem
248+
err = json.NewDecoder(res.Body).Decode(&got)
249+
Expect(err).Should(BeNil())
250+
Expect(len(got)).Should(Equal(1))
251+
252+
got = filterContainerList(got)
253+
Expect(got).Should(ContainElements(want))
254+
})
232255
})
233256
}
234257

0 commit comments

Comments
 (0)