diff --git a/cmd/crictl/events.go b/cmd/crictl/events.go new file mode 100644 index 0000000000..36a5317808 --- /dev/null +++ b/cmd/crictl/events.go @@ -0,0 +1,102 @@ +/* +Copyright 2023 The Kubernetes 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" + "io" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + internalapi "k8s.io/cri-api/pkg/apis" + pb "k8s.io/cri-api/pkg/apis/runtime/v1" +) + +var eventsCommand = &cli.Command{ + Name: "events", + Usage: "Stream the events of containers", + Aliases: []string{"event"}, + UseShortOptionHandling: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Value: "json", + Usage: "Output format, One of: json|yaml|go-template", + }, + &cli.StringFlag{ + Name: "template", + Usage: "The template string is only used when output is go-template; The Template format is golang template", + }, + }, + Action: func(c *cli.Context) error { + if c.NArg() != 0 { + return cli.ShowSubcommandHelp(c) + } + + switch format := c.String("output"); format { + case "json", "yaml": + if len(c.String("template")) > 0 { + return fmt.Errorf("template can't be used with %q format", format) + } + case "go-template": + if err := validateTemplate(c.String(("template"))); err != nil { + return fmt.Errorf("failed to parse go-template: %w", err) + } + default: + return fmt.Errorf("don't support %q format", format) + } + + runtimeClient, err := getRuntimeService(c, 0) + if err != nil { + return err + } + + if err = Events(c, runtimeClient); err != nil { + return fmt.Errorf("getting container events: %w", err) + } + + return nil + }, +} + +func Events(cliContext *cli.Context, client internalapi.RuntimeService) error { + errCh := make(chan error, 1) + + containerEventsCh := make(chan *pb.ContainerEventResponse) + go func() { + logrus.Debug("getting container events") + err := client.GetContainerEvents(containerEventsCh) + if err == io.EOF { + errCh <- nil + return + } + errCh <- err + }() + + for { + select { + case err := <-errCh: + return err + case e := <-containerEventsCh: + err := outputEvent(e, cliContext.String("output"), cliContext.String("template")) + if err != nil { + fmt.Printf("failed to format container event with the error: %s\n", err) + } + } + } +} diff --git a/cmd/crictl/main.go b/cmd/crictl/main.go index 14ea45a0b8..7836a6b369 100644 --- a/cmd/crictl/main.go +++ b/cmd/crictl/main.go @@ -179,6 +179,7 @@ func main() { completionCommand, checkpointContainerCommand, runtimeConfigCommand, + eventsCommand, } runtimeEndpointUsage := fmt.Sprintf("Endpoint of CRI container runtime "+ diff --git a/cmd/crictl/templates.go b/cmd/crictl/templates.go index 222aa157f7..e4e23c85e6 100644 --- a/cmd/crictl/templates.go +++ b/cmd/crictl/templates.go @@ -68,3 +68,8 @@ func tmplExecuteRawJSON(tmplStr string, rawJSON string) (string, error) { } return o.String(), nil } + +func validateTemplate(tmplStr string) error { + _, err := template.New("").Parse(tmplStr) + return err +} diff --git a/cmd/crictl/util.go b/cmd/crictl/util.go index c3970bd8bb..48f638e305 100644 --- a/cmd/crictl/util.go +++ b/cmd/crictl/util.go @@ -260,6 +260,34 @@ func outputStatusInfo(status string, info map[string]string, format string, tmpl return nil } +func outputEvent(event proto.Message, format string, tmplStr string) error { + switch format { + case "yaml": + err := outputProtobufObjAsYAML(event) + if err != nil { + return err + } + case "json": + err := outputProtobufObjAsJSON(event) + if err != nil { + return err + } + case "go-template": + jsonEvent, err := protobufObjectToJSON(event) + if err != nil { + return err + } + output, err := tmplExecuteRawJSON(tmplStr, jsonEvent) + if err != nil { + return err + } + fmt.Println(output) + default: + fmt.Printf("Don't support %q format\n", format) + } + return nil +} + func parseLabelStringSlice(ss []string) (map[string]string, error) { labels := make(map[string]string) for _, s := range ss { diff --git a/docs/crictl.md b/docs/crictl.md index da4a843e6c..1eb4270da0 100644 --- a/docs/crictl.md +++ b/docs/crictl.md @@ -65,6 +65,7 @@ COMMANDS: - `statsp`: List pod(s) resource usage statistics - `completion`: Output bash shell completion code - `checkpoint`: Checkpoint one or more running containers +- `events, event`: Stream the events of containers - `help, h`: Shows a list of commands or help for one command `crictl` by default connects on Unix to: diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go new file mode 100644 index 0000000000..b0f25a2c3f --- /dev/null +++ b/test/e2e/events_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2023 The Kubernetes 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 e2e + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +// The actual test suite +var _ = t.Describe("events options validation", func() { + It("should fail with not supported output format", func() { + t.CrictlExpectFailure("events --output=ini", "", "don't support .* format") + }) + + It("should fail with template set for non go-template format", func() { + t.CrictlExpectFailure("events --template={{.containerID}}", "", "template can't be used with .* format") + }) + + It("should fail with bad template set for go-template format", func() { + t.CrictlExpectFailure("events --output=go-template --template={{", "", "failed to parse go-template") + }) +}) + +// The actual test suite +var _ = t.Describe("events", func() { + var ( + endpoint, testDir string + crio *Session + ) + BeforeEach(func() { + endpoint, testDir, crio = t.StartCrio() + }) + + AfterEach(func() { + t.StopCrio(testDir, crio) + }) + + It("should succeed", func() { + session := t.CrictlWithEndpointNoWait(endpoint, "events") + defer session.Terminate() + Expect(session.Out).ToNot(Say("unknown method GetContainerEvents")) // no errors + }) +}) diff --git a/test/framework/framework.go b/test/framework/framework.go index 02ae9a19c3..adcc879222 100644 --- a/test/framework/framework.go +++ b/test/framework/framework.go @@ -88,6 +88,11 @@ func (t *TestFramework) CrictlWithEndpoint(endpoint, args string) *Session { return lcmd("crictl --runtime-endpoint=%s %s", endpoint, args).Wait(time.Minute) } +// Run crictl on the specified endpoint and return the resulting session without wait +func (t *TestFramework) CrictlWithEndpointNoWait(endpoint, args string) *Session { + return lcmd("crictl --runtime-endpoint=%s %s", endpoint, args) +} + // Run crictl and expect exit, expectedOut, expectedErr func (t *TestFramework) CrictlExpect( endpoint, args string, exit int, expectedOut, expectedErr string, @@ -136,9 +141,9 @@ func (t *TestFramework) CrictlExpectFailureWithEndpoint( func SetupCrio() string { const ( crioURL = "https://github.com/cri-o/cri-o" - crioVersion = "v1.23.1" + crioVersion = "v1.26.4" conmonURL = "https://github.com/containers/conmon" - conmonVersion = "v2.0.32" + conmonVersion = "v2.1.7" ) tmpDir := filepath.Join(os.TempDir(), "crio-tmp") @@ -202,7 +207,8 @@ func (t *TestFramework) StartCrio() (string, string, *Session) { " --cni-config-dir=%s"+ " --root=%s"+ " --runroot=%s"+ - " --pinns-path=%s", + " --pinns-path=%s"+ + " --enable-pod-events", filepath.Join(tmpDir, "bin", "crio"), filepath.Join(t.crioDir, "crio.conf"), endpoint,