Skip to content

Commit

Permalink
feat: adding filter infrastructure + status and event filter
Browse files Browse the repository at this point in the history
Signed-off-by: CodeChanning <[email protected]>
  • Loading branch information
CodeChanning authored and Shubhranshu153 committed Aug 13, 2024
1 parent d3280a9 commit 4d60439
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 31 deletions.
6 changes: 6 additions & 0 deletions cmd/nerdctl/system_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func newEventsCommand() *cobra.Command {
eventsCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
})
eventsCommand.Flags().StringSliceP("filter", "f", []string{}, "Filter matches containers based on given conditions")
return eventsCommand
}

Expand All @@ -51,10 +52,15 @@ func processSystemEventsOptions(cmd *cobra.Command) (types.SystemEventsOptions,
if err != nil {
return types.SystemEventsOptions{}, err
}
filters, err := cmd.Flags().GetStringSlice("filter")
if err != nil {
return types.SystemEventsOptions{}, err
}
return types.SystemEventsOptions{
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
Format: format,
Filters: filters,
}, nil
}

Expand Down
111 changes: 111 additions & 0 deletions cmd/nerdctl/system_events_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"strings"
"testing"
"time"

"github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
)

func testEventFilter(t *testing.T, args ...string) string {
t.Parallel()
base := testutil.NewBase(t)
testContainerName := testutil.Identifier(t)
defer base.Cmd("rm", "-f", testContainerName).Run()

fullArgs := []string{"events", "--filter"}
fullArgs = append(fullArgs, args...)
fullArgs = append(fullArgs,
"--format",
"json",
)

eventsCmd := base.Cmd(fullArgs...).Start()
base.Cmd("run", "--rm", testutil.CommonImage).Start()
time.Sleep(3 * time.Second)
return eventsCmd.Stdout()
}

func TestEventFilters(t *testing.T) {

type testCase struct {
name string
args []string
nerdctlOut string
dockerOut string
dockerSkip bool
}
testCases := []testCase{
{
name: "CapitializedFilter",
args: []string{"event=START"},
nerdctlOut: "\"Status\":\"start\"",
dockerOut: "\"status\":\"start\"",
dockerSkip: true,
},
{
name: "StartEventFilter",
args: []string{"event=start"},
nerdctlOut: "\"Status\":\"start\"",
dockerOut: "\"status\":\"start\"",
dockerSkip: false,
},
{
name: "UnsupportedEventFilter",
args: []string{"event=unknown"},
nerdctlOut: "\"Status\":\"unknown\"",
dockerSkip: true,
},
{
name: "StatusFilter",
args: []string{"status=start"},
nerdctlOut: "\"Status\":\"start\"",
dockerOut: "\"status\":\"start\"",
dockerSkip: false,
},
{
name: "UnsupportedStatusFilter",
args: []string{"status=unknown"},
nerdctlOut: "\"Status\":\"unknown\"",
dockerSkip: true,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
actualOut := testEventFilter(t, tc.args...)
errorMsg := fmt.Sprintf("%s failed;\nActual Filter Result: '%s'", tc.name, actualOut)

isDocker := testutil.GetTarget() == testutil.Docker
if isDocker && tc.dockerSkip {
t.Skip("test is incompatible with Docker")
}

if isDocker {
assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
} else {
assert.Equal(t, true, strings.Contains(actualOut, tc.nerdctlOut), errorMsg)
}
})
}
}
4 changes: 3 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1258,8 +1258,10 @@ Usage: `nerdctl events [OPTIONS]`
Flags:

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

Unimplemented `docker events` flags: `--filter`, `--since`, `--until`
Unimplemented `docker events` flags: `--since`, `--until`

### :whale: nerdctl info

Expand Down
2 changes: 2 additions & 0 deletions pkg/api/types/system_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type SystemEventsOptions struct {
GOptions GlobalCommandOptions
// Format the output using the given Go template, e.g, '{{json .}}
Format string
// Filter events based on given conditions
Filters []string
}

// SystemPruneOptions specifies options for `nerdctl system prune`.
Expand Down
151 changes: 121 additions & 30 deletions pkg/cmd/system/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,101 @@ type EventOut struct {
type Status string

const (
START Status = "START"
UNKNOWN Status = "UNKNOWN"
START Status = "start"
UNKNOWN Status = "unknown"
)

var statuses = [...]Status{START, UNKNOWN}

func isStatus(status string) bool {
status = strings.ToLower(status)

for _, supportedStatus := range statuses {
if string(supportedStatus) == status {
return true
}
}

return false
}

func TopicToStatus(topic string) Status {
if strings.Contains(strings.ToUpper(topic), string(START)) {
if strings.Contains(strings.ToLower(topic), string(START)) {
return START
}

return UNKNOWN
}

// EventFilter for filtering events
type EventFilter func(*EventOut) bool

// generateEventFilter is similar to Podman implementation:
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, error) {
switch strings.ToUpper(filter) {
case "EVENT", "STATUS":
return func(e *EventOut) bool {
if !isStatus(string(e.Status)) {
return false
}

return strings.EqualFold(string(e.Status), filterValue)
}, nil
}

return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter)
}

// parseFilter is similar to Podman implementation:
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L96
func parseFilter(filter string) (string, string, error) {
filterSplit := strings.SplitN(filter, "=", 2)
if len(filterSplit) != 2 {
return "", "", fmt.Errorf("%s is an invalid filter", filter)
}
return filterSplit[0], filterSplit[1], nil
}

// applyFilters is similar to Podman implementation:
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L106
func applyFilters(event *EventOut, filterMap map[string][]EventFilter) bool {
for _, filters := range filterMap {
match := false
for _, filter := range filters {
if filter(event) {
match = true
break
}
}
if !match {
return false
}
}
return true
}

// generateEventFilters is similar to Podman implementation:
// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
func generateEventFilters(filters []string) (map[string][]EventFilter, error) {
filterMap := make(map[string][]EventFilter)
for _, filter := range filters {
key, val, err := parseFilter(filter)
if err != nil {
return nil, err
}
filterFunc, err := generateEventFilter(key, val)
if err != nil {
return nil, err
}
filterSlice := filterMap[key]
filterSlice = append(filterSlice, filterFunc)
filterMap[key] = filterSlice
}

return filterMap, nil
}

// Events is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/events/events.go
func Events(ctx context.Context, client *containerd.Client, options types.SystemEventsOptions) error {
eventsClient := client.EventService()
Expand All @@ -77,6 +160,10 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
return err
}
}
filterMap, err := generateEventFilters(options.Filters)
if err != nil {
return err
}
for {
var e *events.Envelope
select {
Expand All @@ -99,37 +186,41 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
continue
}
}
if tmpl != nil {
var data map[string]interface{}
err := json.Unmarshal(out, &data)
if err != nil {
log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
} else {
_, ok := data["container_id"]
if ok {
id = data["container_id"].(string)
}
var data map[string]interface{}
err := json.Unmarshal(out, &data)
if err != nil {
log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
} else {
_, ok := data["container_id"]
if ok {
id = data["container_id"].(string)
}
}

out := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
var b bytes.Buffer
if err := tmpl.Execute(&b, out); err != nil {
return err
}
if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
return err
}
} else {
if _, err := fmt.Fprintln(
options.Stdout,
e.Timestamp,
e.Namespace,
e.Topic,
string(out),
); err != nil {
return err
eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
match := applyFilters(&eOut, filterMap)
if match {
if tmpl != nil {
var b bytes.Buffer
if err := tmpl.Execute(&b, eOut); err != nil {
return err
}
if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
return err
}
} else {
if _, err := fmt.Fprintln(
options.Stdout,
e.Timestamp,
e.Namespace,
e.Topic,
string(out),
); err != nil {
return err
}
}
}

}
}
}

0 comments on commit 4d60439

Please sign in to comment.