Files
Samuel Karp 2543d23905 ctr: disable slice flag separator on commands
In urfave/cli/v3, child commands do not inherit
DisableSliceFlagSeparator from the root command, unlike in
urfave/cli/v2. When containerd migrated to v3 in commit f72829f36,
subcommands inadvertently reverted to splitting slice flags on commas,
breaking flags that expect comma-separated values such as
--mount type=bind,src=/tmp,dst=/host,options=rbind:ro.

Explicitly set DisableSliceFlagSeparator on all cli.Command structs in
cmd/ctr/commands, restoring the behavior where slice flags are not
split on commas.

Fixes #14181

Assisted-by: Antigravity
Signed-off-by: Samuel Karp <samuelkarp@google.com>
2026-09-17 19:27:36 +00:00

202 lines
4.9 KiB
Go

/*
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 plugins
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/v2/cmd/ctr/commands"
"github.com/containerd/platforms"
pluginutils "github.com/containerd/plugin"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/urfave/cli/v3"
"google.golang.org/grpc/codes"
)
// Command is a cli command that outputs plugin information
var Command = &cli.Command{
Name: "plugins",
Aliases: []string{"plugin"},
Usage: "Provides information about containerd plugins",
Commands: []*cli.Command{
listCommand,
inspectRuntimeCommand,
},
DisableSliceFlagSeparator: true,
}
var listCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "Lists containerd plugins",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Print only the plugin ids",
},
&cli.BoolFlag{
Name: "detailed",
Aliases: []string{"d"},
Usage: "Print detailed information about each plugin",
},
},
DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
var (
quiet = cmd.Bool("quiet")
detailed = cmd.Bool("detailed")
)
client, ctx, cancel, err := commands.NewClient(ctx, cmd)
if err != nil {
return err
}
defer cancel()
ps := client.IntrospectionService()
response, err := ps.Plugins(ctx, cmd.Args().Slice()...)
if err != nil {
return err
}
if quiet {
for _, plugin := range response.Plugins {
fmt.Println(plugin.ID)
}
return nil
}
w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
if detailed {
first := true
for _, plugin := range response.Plugins {
if !first {
fmt.Fprintln(w, "\t\t\t")
}
first = false
fmt.Fprintln(w, "Type:\t", plugin.Type)
fmt.Fprintln(w, "ID:\t", plugin.ID)
if len(plugin.Requires) > 0 {
fmt.Fprintln(w, "Requires:\t")
for _, r := range plugin.Requires {
fmt.Fprintln(w, "\t", r)
}
}
if len(plugin.Platforms) > 0 {
fmt.Fprintln(w, "Platforms:\t", prettyPlatforms(plugin.Platforms))
}
if len(plugin.Exports) > 0 {
fmt.Fprintln(w, "Exports:\t")
for k, v := range plugin.Exports {
fmt.Fprintln(w, "\t", k, "\t", v)
}
}
if len(plugin.Capabilities) > 0 {
fmt.Fprintln(w, "Capabilities:\t", strings.Join(plugin.Capabilities, ","))
}
if plugin.InitErr != nil {
fmt.Fprintln(w, "Error:\t")
fmt.Fprintln(w, "\t Code:\t", codes.Code(plugin.InitErr.Code))
fmt.Fprintln(w, "\t Message:\t", plugin.InitErr.Message)
}
}
return w.Flush()
}
fmt.Fprintln(w, "TYPE\tID\tPLATFORMS\tSTATUS\t")
for _, plugin := range response.Plugins {
status := "ok"
if plugin.InitErr != nil {
if strings.Contains(plugin.InitErr.Message, pluginutils.ErrSkipPlugin.Error()) {
status = "skip"
} else {
status = "error"
}
}
var platformColumn = "-"
if len(plugin.Platforms) > 0 {
platformColumn = prettyPlatforms(plugin.Platforms)
}
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n",
plugin.Type,
plugin.ID,
platformColumn,
status,
); err != nil {
return err
}
}
return w.Flush()
},
}
func prettyPlatforms(pspb []*types.Platform) string {
psm := map[string]struct{}{}
for _, p := range pspb {
psm[platforms.Format(v1.Platform{
OS: p.OS,
Architecture: p.Architecture,
Variant: p.Variant,
})] = struct{}{}
}
var ps []string
for p := range psm {
ps = append(ps, p)
}
sort.Stable(sort.StringSlice(ps))
return strings.Join(ps, ",")
}
var inspectRuntimeCommand = &cli.Command{
Name: "inspect-runtime",
Usage: "Display runtime info",
ArgsUsage: "[flags]",
Flags: commands.RuntimeFlags,
Action: func(ctx context.Context, cmd *cli.Command) error {
rt := cmd.String("runtime")
rtOptions, err := commands.RuntimeOptions(cmd)
if err != nil {
return err
}
client, ctx, cancel, err := commands.NewClient(ctx, cmd)
if err != nil {
return err
}
defer cancel()
rtInfo, err := client.RuntimeInfo(ctx, rt, rtOptions)
if err != nil {
return err
}
j, err := json.MarshalIndent(rtInfo, "", " ")
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.Writer, string(j))
return err
},
DisableSliceFlagSeparator: true,
}