diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index 431abdafb0b..79acb2a8b1c 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -38,6 +38,18 @@ func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.Testable return cmd } +func testEventLabelFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("pull", testutil.CommonImage) + + cmd := helpers.Command("events", "--filter", data.Labels().Get("filter"), "--format", "json") + cmd.WithTimeout(10 * time.Second) + cmd.Background() + + helpers.Ensure("run", "--rm", "--label", data.Labels().Get("containerLabel"), testutil.CommonImage) + + return cmd +} + func TestEventFilters(t *testing.T) { testCase := nerdtest.Setup() @@ -115,6 +127,36 @@ func TestEventFilters(t *testing.T) { "output": "\"Status\":\"unknown\"", }), }, + { + Description: "LabelFilter", + Command: testEventLabelFilterExecutor, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeTimeout, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "filter": "label=com.example.app=myapp", + "containerLabel": "com.example.app=myapp", + "output": "\"Status\":\"start\"", + }), + }, + { + Description: "LabelKeyOnlyFilter", + Command: testEventLabelFilterExecutor, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeTimeout, + Output: expect.Contains(data.Labels().Get("output")), + } + }, + Data: test.WithLabels(map[string]string{ + "filter": "label=com.example.app", + "containerLabel": "com.example.app=myapp", + "output": "\"Status\":\"start\"", + }), + }, } testCase.Run(t) diff --git a/pkg/cmd/system/events.go b/pkg/cmd/system/events.go index a544f071b57..66882d31eae 100644 --- a/pkg/cmd/system/events.go +++ b/pkg/cmd/system/events.go @@ -44,6 +44,7 @@ type EventOut struct { Topic string Status Status Event string + Labels map[string]string } type Status string @@ -90,6 +91,20 @@ func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, er return strings.EqualFold(string(e.Status), filterValue) }, nil + case "LABEL": + return func(e *EventOut) bool { + if len(e.Labels) == 0 { + return false + } + + parts := strings.SplitN(filterValue, "=", 2) + if len(parts) == 1 { + _, ok := e.Labels[parts[0]] + return ok + } + + return e.Labels[parts[0]] == parts[1] + }, nil } return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter) @@ -175,6 +190,7 @@ func Events(ctx context.Context, client *containerd.Client, options types.System if e != nil { var out []byte var id string + labels := map[string]string{} if e.Event != nil { v, err := typeurl.UnmarshalAny(e.Event) if err != nil { @@ -197,8 +213,14 @@ func Events(ctx context.Context, client *containerd.Client, options types.System id = data["container_id"].(string) } } - - eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)} + if id != "" { + if container, err := client.ContainerService().Get(ctx, id); err != nil { + log.G(ctx).WithError(err).WithField("containerID", id).Debug("failed to retrieve container labels") + } else { + labels = container.Labels + } + } + eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out), labels} match := applyFilters(&eOut, filterMap) if match { if tmpl != nil {