Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding filter infrastructure + status and event filter #3247

Merged
merged 1 commit into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/v2/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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment to explain the reason?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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 @@ -1275,8 +1275,10 @@ Usage: `nerdctl events [OPTIONS]`
Flags:

- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI @CodeChanning

The new flag docs can be added here. :-)

Copy link
Contributor Author

@CodeChanning CodeChanning Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added events --filter docs. PTAL and let me know if this is sufficient. @yankay

- :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
}
}
}

}
}
}