From d1d09eb37ae11e37a20968e3ba1f760b05a28206 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Fri, 20 Sep 2024 09:36:05 +0200 Subject: [PATCH] crictl exec: allow running single command in multiple containers This patch adds the following filter flags to `crictl exec`: ``` --image value Exec command for all containers matching this container image filter --label value [ --label value ] Exec command for all containers matching this key=value label filter --last value, -n value Exec command for all last n containers, set to 0 for unlimited (default: 0) --latest, -l Exec command for the most recently created container (default: false) --name value Exec command for all containers matching this name filter regular expression pattern --pod value, -p value Exec command for all containers matching this pod ID filter --state value Exec command for all containers matching this container state filter ``` If none of the above filters are set, then crictl will assume that the first provided argument is the container ID. If one of them is set, then crictl will filter and execute for all containers: ``` > sudo ./build/bin/linux/amd64/crictl exec --name pod ps aux 8f92b841d898828833d7dc55245e5df91c517cb0fce8a61f5def7cb9e432530c: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2472 1344 ? Ss 07:09 0:00 /bin/sleep 6000 6000 root 67 0.0 0.0 5536 2700 ? Rs 07:38 0:00 ps aux 39f78a7b1835b8363a519424031ddbb370b743afad46cc401eeb73717c84e57d: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2472 1500 ? Ss 07:08 0:00 /bin/sleep 6000 6000 root 122 0.0 0.0 5536 2732 ? Rs 07:38 0:00 ps aux ``` The container ID is printed when a filter command has been used to ensure existing backwards compatibility. Signed-off-by: Sascha Grunert --- cmd/crictl/exec.go | 137 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 21 deletions(-) diff --git a/cmd/crictl/exec.go b/cmd/crictl/exec.go index 3a8bcdee32..f470c5101b 100644 --- a/cmd/crictl/exec.go +++ b/cmd/crictl/exec.go @@ -41,14 +41,16 @@ const ( transportFlag = "transport" transportWebsocket = "websocket" transportSpdy = "spdy" -) -const detachSequence = "ctrl-p,ctrl-q" + detachSequence = "ctrl-p,ctrl-q" +) var runtimeExecCommand = &cli.Command{ - Name: "exec", - Usage: "Run a command in a running container", - ArgsUsage: "CONTAINER-ID COMMAND [ARG...]", + Name: "exec", + Usage: "Run a command in a running container", + ArgsUsage: "[CONTAINER-ID] COMMAND [ARG...]", + Description: `The CONTAINER-ID is only required if none of the following filter flags are set: +--image, --label, --last, --latest, --name, --pod, --state`, UseShortOptionHandling: true, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -76,9 +78,40 @@ var runtimeExecCommand = &cli.Command{ Value: transportSpdy, Usage: fmt.Sprintf("Transport protocol to use, one of: %s|%s", transportSpdy, transportWebsocket), }, + &cli.StringFlag{ + Name: "name", + Usage: "Exec command for all containers matching this name filter regular expression pattern", + }, + &cli.StringFlag{ + Name: "pod", + Aliases: []string{"p"}, + Usage: "Exec command for all containers matching this pod ID filter", + }, + &cli.StringFlag{ + Name: "image", + Usage: "Exec command for all containers matching this container image filter", + }, + &cli.StringFlag{ + Name: "state", + Usage: "Exec command for all containers matching this container state filter", + }, + &cli.StringSliceFlag{ + Name: "label", + Usage: "Exec command for all containers matching this key=value label filter", + }, + &cli.BoolFlag{ + Name: "latest", + Aliases: []string{"l"}, + Usage: "Exec command for the most recently created container", + }, + &cli.IntFlag{ + Name: "last", + Aliases: []string{"n"}, + Usage: "Exec command for all last n containers, set to 0 for unlimited", + }, }, Action: func(c *cli.Context) error { - if c.NArg() < 2 { + if c.NArg() < 1 { return cli.ShowSubcommandHelp(c) } @@ -87,32 +120,94 @@ var runtimeExecCommand = &cli.Command{ return err } + // Assume a regular exec where the first arg is the container ID. + ids := []string{c.Args().First()} + cmd := c.Args().Slice()[1:] + + // If any of the filter flags are set, then we assume that no + // CONTAINER-ID is provided as CLI parameter. + if c.IsSet("name") || + c.IsSet("pod") || + c.IsSet("image") || + c.IsSet("state") || + c.IsSet("label") || + c.IsSet("latest") || + c.IsSet("last") { + + ids = []string{} + cmd = c.Args().Slice() + + 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 containers found per filter flags") + return cli.ShowSubcommandHelp(c) + } + } else if c.NArg() < 2 { + return cli.ShowSubcommandHelp(c) + } + opts := execOptions{ - id: c.Args().First(), timeout: c.Int64("timeout"), tty: c.Bool("tty"), stdin: c.Bool("interactive"), - cmd: c.Args().Slice()[1:], + cmd: cmd, transport: c.String(transportFlag), } - if c.Bool("sync") { - exitCode, err := ExecSync(runtimeClient, opts) - if err != nil { - return fmt.Errorf("execing command in container synchronously: %w", err) + + outputID := false + if len(ids) > 1 { + outputID = true + } + + for _, id := range ids { + opts.id = id + + if outputID { + fmt.Println(id + ":") } - if exitCode != 0 { - return cli.Exit("non-zero exit code", exitCode) + + if c.Bool("sync") { + exitCode, err := ExecSync(runtimeClient, opts) + if err != nil { + return fmt.Errorf("execing command in container synchronously: %w", err) + } + if exitCode != 0 { + return cli.Exit("non-zero exit code", exitCode) + } + continue } - return nil - } - ctx, cancel := context.WithCancel(c.Context) - defer cancel() + ctx, cancel := context.WithCancel(c.Context) + defer cancel() + err = Exec(ctx, runtimeClient, opts) + if err != nil { + return fmt.Errorf("execing command in container: %w", err) + } - err = Exec(ctx, runtimeClient, opts) - if err != nil { - return fmt.Errorf("execing command in container: %w", err) } + return nil }, }