From 7c06bc4a5e5736a2f1ea93cc7bf35388141ee799 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Tue, 6 Aug 2024 12:30:17 +0200 Subject: [PATCH] Improve `crictl inspect[pi]` commands to allow filtering The commands now allow filtering based on various fields or just to inspect all pods, containers or image. Signed-off-by: Sascha Grunert --- cmd/crictl/container.go | 84 ++++++++++++++++++++++++++++++++++---- cmd/crictl/image.go | 58 ++++++++++++++++++--------- cmd/crictl/sandbox.go | 89 ++++++++++++++++++++++++++++++++++------- 3 files changed, 191 insertions(+), 40 deletions(-) diff --git a/cmd/crictl/container.go b/cmd/crictl/container.go index 4a5e0d32e0..9d98155bba 100644 --- a/cmd/crictl/container.go +++ b/cmd/crictl/container.go @@ -502,19 +502,78 @@ var containerStatusCommand = &cli.Command{ Name: "template", Usage: "The template string is only used when output is go-template; The Template format is golang template", }, + &cli.StringFlag{ + Name: "name", + Usage: "Filter by container name regular expression pattern", + }, + &cli.StringFlag{ + Name: "pod", + Aliases: []string{"p"}, + Usage: "Filter by pod id", + }, + &cli.StringFlag{ + Name: "image", + Usage: "Filter by container image", + }, + &cli.StringFlag{ + Name: "state", + Aliases: []string{"s"}, + Usage: "Filter by container state", + }, + &cli.StringSliceFlag{ + Name: "label", + Usage: "Filter by key=value label", + }, + &cli.BoolFlag{ + Name: "latest", + Aliases: []string{"l"}, + Usage: "Show the most recently created container (includes all states)", + }, + &cli.IntFlag{ + Name: "last", + Aliases: []string{"n"}, + Usage: "Show last n recently created containers (includes all states). Set 0 for unlimited.", + }, }, Action: func(c *cli.Context) error { - if c.NArg() == 0 { - return errors.New("ID cannot be empty") - } runtimeClient, err := getRuntimeService(c, 0) if err != nil { return err } + ids := c.Args().Slice() + + if len(ids) == 0 { + opts := &listOptions{ + nameRegexp: c.String("name"), + podID: c.String("pod"), + image: c.String("image"), + state: c.String("state"), + latest: c.Bool("latest"), + last: c.Int("last"), + } + opts.labels, err = parseLabelStringSlice(c.StringSlice("label")) + if err != nil { + return err + } + + ctrs, err := ListContainers(runtimeClient, opts) + if err != nil { + return fmt.Errorf("listing containers: %w", err) + } + for _, ctr := range ctrs { + ids = append(ids, ctr.GetId()) + } + } + + if len(ids) == 0 { + logrus.Error("No IDs provided or nothing found per filter") + return cli.ShowSubcommandHelp(c) + } + if err := containerStatus( runtimeClient, - c.Args().Slice(), + ids, c.String("output"), c.String("template"), c.Bool("quiet"), @@ -633,7 +692,7 @@ var listContainersCommand = &cli.Command{ return err } - if err = ListContainers(runtimeClient, imageClient, opts); err != nil { + if err = OutputContainers(runtimeClient, imageClient, opts); err != nil { return fmt.Errorf("listing containers: %w", err) } return nil @@ -1080,7 +1139,7 @@ func outputContainerStatusTable(r *pb.ContainerStatusResponse, verbose bool) { // ListContainers sends a ListContainerRequest to the server, and parses // the returned ListContainerResponse. -func ListContainers(runtimeClient internalapi.RuntimeService, imageClient internalapi.ImageManagerService, opts *listOptions) error { +func ListContainers(runtimeClient internalapi.RuntimeService, opts *listOptions) ([]*pb.Container, error) { filter := &pb.ContainerFilter{} if opts.id != "" { filter.Id = opts.id @@ -1124,9 +1183,18 @@ func ListContainers(runtimeClient internalapi.RuntimeService, imageClient intern }) logrus.Debugf("ListContainerResponse: %v", r) if err != nil { - return err + return nil, fmt.Errorf("call list containers RPC: %w", err) + } + return getContainersList(r, opts), nil +} + +// OutputContainers sends a ListContainerRequest to the server, and parses +// the returned ListContainerResponse for output. +func OutputContainers(runtimeClient internalapi.RuntimeService, imageClient internalapi.ImageManagerService, opts *listOptions) error { + r, err := ListContainers(runtimeClient, opts) + if err != nil { + return fmt.Errorf("list containers: %w", err) } - r = getContainersList(r, opts) switch opts.output { case outputTypeJSON: diff --git a/cmd/crictl/image.go b/cmd/crictl/image.go index 36c74ea416..6dff03196b 100644 --- a/cmd/crictl/image.go +++ b/cmd/crictl/image.go @@ -191,20 +191,11 @@ var listImageCommand = &cli.Command{ return err } - r, err := ListImages(imageClient, c.Args().First()) + r, err := ListImages(imageClient, c.Args().First(), c.StringSlice("filter")) if err != nil { return fmt.Errorf("listing images: %w", err) } - sort.Sort(imageByRef(r.Images)) - - if len(c.StringSlice("filter")) > 0 && len(r.Images) > 0 { - r.Images, err = filterImagesList(r.Images, c.StringSlice("filter")) - if err != nil { - return fmt.Errorf("listing images: %w", err) - } - } - switch c.String("output") { case outputTypeJSON: return outputProtobufObjAsJSON(r) @@ -303,11 +294,17 @@ var imageStatusCommand = &cli.Command{ Name: "template", Usage: "The template string is only used when output is go-template; The Template format is golang template", }, + &cli.StringFlag{ + Name: "name", + Usage: "Filter by image name", + }, + &cli.StringSliceFlag{ + Name: "filter", + Aliases: []string{"f"}, + Usage: "Filter output based on provided conditions.\nAvailable filters: \n* dangling=(boolean - true or false)\n* reference=/regular expression/\n* before=[:]||\n* since=[:]||\nMultiple filters can be combined together.", + }, }, Action: func(c *cli.Context) error { - if c.NArg() == 0 { - return cli.ShowSubcommandHelp(c) - } imageClient, err := getImageService(c) if err != nil { return err @@ -320,10 +317,25 @@ var imageStatusCommand = &cli.Command{ } tmplStr := c.String("template") - statuses := []statusData{} - for i := range c.NArg() { - id := c.Args().Get(i) + ids := c.Args().Slice() + + if len(ids) == 0 { + r, err := ListImages(imageClient, c.String("name"), c.StringSlice("filter")) + if err != nil { + return fmt.Errorf("listing images: %w", err) + } + for _, img := range r.GetImages() { + ids = append(ids, img.GetId()) + } + } + if len(ids) == 0 { + logrus.Error("No IDs provided or nothing found per filter") + return cli.ShowSubcommandHelp(c) + } + + statuses := []statusData{} + for _, id := range ids { r, err := ImageStatus(imageClient, id, verbose) if err != nil { return fmt.Errorf("image status for %q request: %w", id, err) @@ -684,8 +696,8 @@ func PullImageWithSandbox(client internalapi.ImageManagerService, image string, // ListImages sends a ListImagesRequest to the server, and parses // the returned ListImagesResponse. -func ListImages(client internalapi.ImageManagerService, image string) (*pb.ListImagesResponse, error) { - request := &pb.ListImagesRequest{Filter: &pb.ImageFilter{Image: &pb.ImageSpec{Image: image}}} +func ListImages(client internalapi.ImageManagerService, nameFilter string, conditionFilters []string) (*pb.ListImagesResponse, error) { + request := &pb.ListImagesRequest{Filter: &pb.ImageFilter{Image: &pb.ImageSpec{Image: nameFilter}}} logrus.Debugf("ListImagesRequest: %v", request) res, err := InterruptableRPC(nil, func(ctx context.Context) ([]*pb.Image, error) { return client.ListImages(ctx, request.Filter) @@ -695,6 +707,16 @@ func ListImages(client internalapi.ImageManagerService, image string) (*pb.ListI } resp := &pb.ListImagesResponse{Images: res} logrus.Debugf("ListImagesResponse: %v", resp) + + sort.Sort(imageByRef(resp.Images)) + + if len(conditionFilters) > 0 && len(resp.Images) > 0 { + resp.Images, err = filterImagesList(resp.Images, conditionFilters) + if err != nil { + return nil, fmt.Errorf("filter images: %w", err) + } + } + return resp, nil } diff --git a/cmd/crictl/sandbox.go b/cmd/crictl/sandbox.go index 04a4223af9..7bf2cadb78 100644 --- a/cmd/crictl/sandbox.go +++ b/cmd/crictl/sandbox.go @@ -218,19 +218,71 @@ var podStatusCommand = &cli.Command{ Name: "template", Usage: "The template string is only used when output is go-template; The Template format is golang template", }, + &cli.StringFlag{ + Name: "name", + Usage: "Filter by pod name regular expression pattern", + }, + &cli.StringFlag{ + Name: "namespace", + Usage: "Filter by pod namespace regular expression pattern", + }, + &cli.StringFlag{ + Name: "state", + Aliases: []string{"s"}, + Usage: "Filter by pod state", + }, + &cli.StringSliceFlag{ + Name: "label", + Usage: "Filter by key=value label", + }, + &cli.BoolFlag{ + Name: "latest", + Aliases: []string{"l"}, + Usage: "Show the most recently created pod", + }, + &cli.IntFlag{ + Name: "last", + Aliases: []string{"n"}, + Usage: "Show last n recently created pods. Set 0 for unlimited", + }, }, Action: func(c *cli.Context) error { - if c.NArg() == 0 { - return cli.ShowSubcommandHelp(c) - } runtimeClient, err := getRuntimeService(c, 0) if err != nil { return err } + ids := c.Args().Slice() + + if len(ids) == 0 { + opts := &listOptions{ + nameRegexp: c.String("name"), + podNamespaceRegexp: c.String("namespace"), + state: c.String("state"), + latest: c.Bool("latest"), + last: c.Int("last"), + } + opts.labels, err = parseLabelStringSlice(c.StringSlice("label")) + if err != nil { + return fmt.Errorf("parse label string slice: %w", err) + } + sbs, err := ListPodSandboxes(runtimeClient, opts) + if err != nil { + return fmt.Errorf("listing pod sandboxes: %w", err) + } + for _, sb := range sbs { + ids = append(ids, sb.GetId()) + } + } + + if len(ids) == 0 { + logrus.Error("No IDs provided or nothing found per filter") + return cli.ShowSubcommandHelp(c) + } + if err := podSandboxStatus( runtimeClient, - c.Args().Slice(), + ids, c.String("output"), c.Bool("quiet"), c.String("template"), @@ -253,30 +305,30 @@ var listPodCommand = &cli.Command{ }, &cli.StringFlag{ Name: "name", - Usage: "filter by pod name regular expression pattern", + Usage: "Filter by pod name regular expression pattern", }, &cli.StringFlag{ Name: "namespace", - Usage: "filter by pod namespace regular expression pattern", + Usage: "Filter by pod namespace regular expression pattern", }, &cli.StringFlag{ Name: "state", Aliases: []string{"s"}, - Usage: "filter by pod state", + Usage: "Filter by pod state", }, &cli.StringSliceFlag{ Name: "label", - Usage: "filter by key=value label", + Usage: "Filter by key=value label", }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"v"}, - Usage: "show verbose info for pods", + Usage: "Show verbose info for pods", }, &cli.BoolFlag{ Name: "quiet", Aliases: []string{"q"}, - Usage: "list only pod IDs", + Usage: "List only pod IDs", }, &cli.StringFlag{ Name: "output", @@ -322,7 +374,7 @@ var listPodCommand = &cli.Command{ if err != nil { return err } - if err = ListPodSandboxes(runtimeClient, opts); err != nil { + if err = OutputPodSandboxes(runtimeClient, opts); err != nil { return fmt.Errorf("listing pod sandboxes: %w", err) } return nil @@ -482,7 +534,7 @@ func outputPodSandboxStatusTable(r *pb.PodSandboxStatusResponse, verbose bool) { // ListPodSandboxes sends a ListPodSandboxRequest to the server, and parses // the returned ListPodSandboxResponse. -func ListPodSandboxes(client internalapi.RuntimeService, opts *listOptions) error { +func ListPodSandboxes(client internalapi.RuntimeService, opts *listOptions) ([]*pb.PodSandbox, error) { filter := &pb.PodSandboxFilter{} if opts.id != "" { filter.Id = opts.id @@ -513,9 +565,18 @@ func ListPodSandboxes(client internalapi.RuntimeService, opts *listOptions) erro }) logrus.Debugf("ListPodSandboxResponse: %v", r) if err != nil { - return err + return nil, fmt.Errorf("call list sandboxes RPC: %w", err) + } + return getSandboxesList(r, opts), nil +} + +// OutputPodSandboxes sends a ListPodSandboxRequest to the server, and parses +// the returned ListPodSandboxResponse for output. +func OutputPodSandboxes(client internalapi.RuntimeService, opts *listOptions) error { + r, err := ListPodSandboxes(client, opts) + if err != nil { + return fmt.Errorf("list pod sandboxes: %w", err) } - r = getSandboxesList(r, opts) switch opts.output { case outputTypeJSON: