Skip to content

Commit 4d60439

Browse files
CodeChanningShubhranshu153
authored andcommitted
feat: adding filter infrastructure + status and event filter
Signed-off-by: CodeChanning <[email protected]>
1 parent d3280a9 commit 4d60439

File tree

5 files changed

+243
-31
lines changed

5 files changed

+243
-31
lines changed

cmd/nerdctl/system_events.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func newEventsCommand() *cobra.Command {
3939
eventsCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
4040
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
4141
})
42+
eventsCommand.Flags().StringSliceP("filter", "f", []string{}, "Filter matches containers based on given conditions")
4243
return eventsCommand
4344
}
4445

@@ -51,10 +52,15 @@ func processSystemEventsOptions(cmd *cobra.Command) (types.SystemEventsOptions,
5152
if err != nil {
5253
return types.SystemEventsOptions{}, err
5354
}
55+
filters, err := cmd.Flags().GetStringSlice("filter")
56+
if err != nil {
57+
return types.SystemEventsOptions{}, err
58+
}
5459
return types.SystemEventsOptions{
5560
Stdout: cmd.OutOrStdout(),
5661
GOptions: globalOptions,
5762
Format: format,
63+
Filters: filters,
5864
}, nil
5965
}
6066

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"testing"
23+
"time"
24+
25+
"github.com/containerd/nerdctl/pkg/testutil"
26+
"gotest.tools/v3/assert"
27+
)
28+
29+
func testEventFilter(t *testing.T, args ...string) string {
30+
t.Parallel()
31+
base := testutil.NewBase(t)
32+
testContainerName := testutil.Identifier(t)
33+
defer base.Cmd("rm", "-f", testContainerName).Run()
34+
35+
fullArgs := []string{"events", "--filter"}
36+
fullArgs = append(fullArgs, args...)
37+
fullArgs = append(fullArgs,
38+
"--format",
39+
"json",
40+
)
41+
42+
eventsCmd := base.Cmd(fullArgs...).Start()
43+
base.Cmd("run", "--rm", testutil.CommonImage).Start()
44+
time.Sleep(3 * time.Second)
45+
return eventsCmd.Stdout()
46+
}
47+
48+
func TestEventFilters(t *testing.T) {
49+
50+
type testCase struct {
51+
name string
52+
args []string
53+
nerdctlOut string
54+
dockerOut string
55+
dockerSkip bool
56+
}
57+
testCases := []testCase{
58+
{
59+
name: "CapitializedFilter",
60+
args: []string{"event=START"},
61+
nerdctlOut: "\"Status\":\"start\"",
62+
dockerOut: "\"status\":\"start\"",
63+
dockerSkip: true,
64+
},
65+
{
66+
name: "StartEventFilter",
67+
args: []string{"event=start"},
68+
nerdctlOut: "\"Status\":\"start\"",
69+
dockerOut: "\"status\":\"start\"",
70+
dockerSkip: false,
71+
},
72+
{
73+
name: "UnsupportedEventFilter",
74+
args: []string{"event=unknown"},
75+
nerdctlOut: "\"Status\":\"unknown\"",
76+
dockerSkip: true,
77+
},
78+
{
79+
name: "StatusFilter",
80+
args: []string{"status=start"},
81+
nerdctlOut: "\"Status\":\"start\"",
82+
dockerOut: "\"status\":\"start\"",
83+
dockerSkip: false,
84+
},
85+
{
86+
name: "UnsupportedStatusFilter",
87+
args: []string{"status=unknown"},
88+
nerdctlOut: "\"Status\":\"unknown\"",
89+
dockerSkip: true,
90+
},
91+
}
92+
93+
for _, tc := range testCases {
94+
tc := tc
95+
t.Run(tc.name, func(t *testing.T) {
96+
actualOut := testEventFilter(t, tc.args...)
97+
errorMsg := fmt.Sprintf("%s failed;\nActual Filter Result: '%s'", tc.name, actualOut)
98+
99+
isDocker := testutil.GetTarget() == testutil.Docker
100+
if isDocker && tc.dockerSkip {
101+
t.Skip("test is incompatible with Docker")
102+
}
103+
104+
if isDocker {
105+
assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
106+
} else {
107+
assert.Equal(t, true, strings.Contains(actualOut, tc.nerdctlOut), errorMsg)
108+
}
109+
})
110+
}
111+
}

docs/command-reference.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1258,8 +1258,10 @@ Usage: `nerdctl events [OPTIONS]`
12581258
Flags:
12591259

12601260
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
1261+
- :whale: `-f, --filter`: Filter containers based on given conditions
1262+
- :whale: `--filter event=<value>`: Event's status. Start is the only supported status.
12611263

1262-
Unimplemented `docker events` flags: `--filter`, `--since`, `--until`
1264+
Unimplemented `docker events` flags: `--since`, `--until`
12631265

12641266
### :whale: nerdctl info
12651267

pkg/api/types/system_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type SystemEventsOptions struct {
3737
GOptions GlobalCommandOptions
3838
// Format the output using the given Go template, e.g, '{{json .}}
3939
Format string
40+
// Filter events based on given conditions
41+
Filters []string
4042
}
4143

4244
// SystemPruneOptions specifies options for `nerdctl system prune`.

pkg/cmd/system/events.go

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,101 @@ type EventOut struct {
4848
type Status string
4949

5050
const (
51-
START Status = "START"
52-
UNKNOWN Status = "UNKNOWN"
51+
START Status = "start"
52+
UNKNOWN Status = "unknown"
5353
)
5454

55+
var statuses = [...]Status{START, UNKNOWN}
56+
57+
func isStatus(status string) bool {
58+
status = strings.ToLower(status)
59+
60+
for _, supportedStatus := range statuses {
61+
if string(supportedStatus) == status {
62+
return true
63+
}
64+
}
65+
66+
return false
67+
}
68+
5569
func TopicToStatus(topic string) Status {
56-
if strings.Contains(strings.ToUpper(topic), string(START)) {
70+
if strings.Contains(strings.ToLower(topic), string(START)) {
5771
return START
5872
}
5973

6074
return UNKNOWN
6175
}
6276

77+
// EventFilter for filtering events
78+
type EventFilter func(*EventOut) bool
79+
80+
// generateEventFilter is similar to Podman implementation:
81+
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
82+
func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, error) {
83+
switch strings.ToUpper(filter) {
84+
case "EVENT", "STATUS":
85+
return func(e *EventOut) bool {
86+
if !isStatus(string(e.Status)) {
87+
return false
88+
}
89+
90+
return strings.EqualFold(string(e.Status), filterValue)
91+
}, nil
92+
}
93+
94+
return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter)
95+
}
96+
97+
// parseFilter is similar to Podman implementation:
98+
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L96
99+
func parseFilter(filter string) (string, string, error) {
100+
filterSplit := strings.SplitN(filter, "=", 2)
101+
if len(filterSplit) != 2 {
102+
return "", "", fmt.Errorf("%s is an invalid filter", filter)
103+
}
104+
return filterSplit[0], filterSplit[1], nil
105+
}
106+
107+
// applyFilters is similar to Podman implementation:
108+
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L106
109+
func applyFilters(event *EventOut, filterMap map[string][]EventFilter) bool {
110+
for _, filters := range filterMap {
111+
match := false
112+
for _, filter := range filters {
113+
if filter(event) {
114+
match = true
115+
break
116+
}
117+
}
118+
if !match {
119+
return false
120+
}
121+
}
122+
return true
123+
}
124+
125+
// generateEventFilters is similar to Podman implementation:
126+
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
127+
func generateEventFilters(filters []string) (map[string][]EventFilter, error) {
128+
filterMap := make(map[string][]EventFilter)
129+
for _, filter := range filters {
130+
key, val, err := parseFilter(filter)
131+
if err != nil {
132+
return nil, err
133+
}
134+
filterFunc, err := generateEventFilter(key, val)
135+
if err != nil {
136+
return nil, err
137+
}
138+
filterSlice := filterMap[key]
139+
filterSlice = append(filterSlice, filterFunc)
140+
filterMap[key] = filterSlice
141+
}
142+
143+
return filterMap, nil
144+
}
145+
63146
// Events is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/events/events.go
64147
func Events(ctx context.Context, client *containerd.Client, options types.SystemEventsOptions) error {
65148
eventsClient := client.EventService()
@@ -77,6 +160,10 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
77160
return err
78161
}
79162
}
163+
filterMap, err := generateEventFilters(options.Filters)
164+
if err != nil {
165+
return err
166+
}
80167
for {
81168
var e *events.Envelope
82169
select {
@@ -99,37 +186,41 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
99186
continue
100187
}
101188
}
102-
if tmpl != nil {
103-
var data map[string]interface{}
104-
err := json.Unmarshal(out, &data)
105-
if err != nil {
106-
log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
107-
} else {
108-
_, ok := data["container_id"]
109-
if ok {
110-
id = data["container_id"].(string)
111-
}
189+
var data map[string]interface{}
190+
err := json.Unmarshal(out, &data)
191+
if err != nil {
192+
log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
193+
} else {
194+
_, ok := data["container_id"]
195+
if ok {
196+
id = data["container_id"].(string)
112197
}
198+
}
113199

114-
out := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
115-
var b bytes.Buffer
116-
if err := tmpl.Execute(&b, out); err != nil {
117-
return err
118-
}
119-
if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
120-
return err
121-
}
122-
} else {
123-
if _, err := fmt.Fprintln(
124-
options.Stdout,
125-
e.Timestamp,
126-
e.Namespace,
127-
e.Topic,
128-
string(out),
129-
); err != nil {
130-
return err
200+
eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
201+
match := applyFilters(&eOut, filterMap)
202+
if match {
203+
if tmpl != nil {
204+
var b bytes.Buffer
205+
if err := tmpl.Execute(&b, eOut); err != nil {
206+
return err
207+
}
208+
if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
209+
return err
210+
}
211+
} else {
212+
if _, err := fmt.Fprintln(
213+
options.Stdout,
214+
e.Timestamp,
215+
e.Namespace,
216+
e.Topic,
217+
string(out),
218+
); err != nil {
219+
return err
220+
}
131221
}
132222
}
223+
133224
}
134225
}
135226
}

0 commit comments

Comments
 (0)