From dda3ea209cbaff85835eeeb23d7b415143c8fabf Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Wed, 10 Aug 2022 06:46:18 -0400 Subject: [PATCH] Add support for podman context as alias to podman system connection Alias podman --conntext -> podman --connection podman context use -> podman system connection default podman context rm -> podman system connection rm podman context create -> podman system connection add podman context ls ->podman system connection ls podman context inspect ->podman system connection ls --json (For specified connections) Podman context is a hidden command, but can be used for existing scripts that assume Docker under the covers. Signed-off-by: Daniel J Walsh --- cmd/podman/root.go | 14 ++-- cmd/podman/system/connection/add.go | 84 ++++++++++++++++++- cmd/podman/system/connection/default.go | 14 ++++ cmd/podman/system/connection/list.go | 56 +++++++++++-- cmd/podman/system/connection/remove.go | 8 ++ cmd/podman/system/context.go | 24 ++++++ .../podman-system-connection-list.1.md | 6 +- pkg/rootless/rootless_linux.c | 1 + test/system/272-system-connection.bats | 63 ++++++++++++-- 9 files changed, 250 insertions(+), 20 deletions(-) create mode 100644 cmd/podman/system/context.go diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 3637b26741b7..32f4d57226c5 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -171,6 +171,9 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { // --connection is not as "special" as --remote so we can wait and process it here conn := cmd.Root().LocalFlags().Lookup("connection") + if conn != nil && !conn.Changed { + conn = cmd.Root().LocalFlags().Lookup("context") + } if conn != nil && conn.Changed { cfg.Engine.ActiveService = conn.Value.String() @@ -227,10 +230,6 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { } } - context := cmd.Root().LocalFlags().Lookup("context") - if context.Value.String() != "default" { - return errors.New("podman does not support swarm, the only --context value allowed is \"default\"") - } if !registry.IsRemote() { if cmd.Flag("cpu-profile").Changed { f, err := os.Create(cfg.CPUProfile) @@ -347,16 +346,15 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { lFlags.StringVarP(&opts.Engine.ActiveService, connectionFlagName, "c", srv, "Connection to use for remote Podman service") _ = cmd.RegisterFlagCompletionFunc(connectionFlagName, common.AutocompleteSystemConnections) + lFlags.StringVar(&opts.Engine.ActiveService, "context", srv, "Docker compatibility matching --connection") + _ = lFlags.MarkHidden("context") + urlFlagName := "url" lFlags.StringVar(&opts.URI, urlFlagName, uri, "URL to access Podman service (CONTAINER_HOST)") _ = cmd.RegisterFlagCompletionFunc(urlFlagName, completion.AutocompleteDefault) lFlags.StringVarP(&opts.URI, "host", "H", uri, "Used for Docker compatibility") _ = lFlags.MarkHidden("host") - // Context option added just for compatibility with DockerCLI. - lFlags.String("context", "default", "Name of the context to use to connect to the daemon (This flag is a NOOP and provided solely for scripting compatibility.)") - _ = lFlags.MarkHidden("context") - identityFlagName := "identity" lFlags.StringVar(&opts.Identity, identityFlagName, ident, "path to SSH identity file, (CONTAINER_SSHKEY)") _ = cmd.RegisterFlagCompletionFunc(identityFlagName, completion.AutocompleteDefault) diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go index f3b61b254038..2730ebfb74a5 100644 --- a/cmd/podman/system/connection/add.go +++ b/cmd/podman/system/connection/add.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "regexp" + "strings" "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/config" @@ -37,6 +38,17 @@ var ( `, } + createCmd = &cobra.Command{ + Use: "create [options] NAME DESTINATION", + Args: cobra.ExactArgs(1), + Short: addCmd.Short, + Long: addCmd.Long, + RunE: create, + ValidArgsFunction: completion.AutocompleteNone, + } + + dockerPath string + cOpts = struct { Identity string Port int @@ -50,7 +62,6 @@ func init() { Command: addCmd, Parent: system.ConnectionCmd, }) - flags := addCmd.Flags() portFlagName := "port" @@ -66,6 +77,21 @@ func init() { _ = addCmd.RegisterFlagCompletionFunc(socketPathFlagName, completion.AutocompleteDefault) flags.BoolVarP(&cOpts.Default, "default", "d", false, "Set connection to be default") + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: createCmd, + Parent: system.ContextCmd, + }) + + flags = createCmd.Flags() + dockerFlagName := "docker" + flags.StringVar(&dockerPath, dockerFlagName, "", "Description of the context") + + _ = createCmd.RegisterFlagCompletionFunc(dockerFlagName, completion.AutocompleteNone) + flags.String("description", "", "Ignored. Just for script compatibility") + flags.String("from", "", "Ignored. Just for script compatibility") + flags.String("kubernetes", "", "Ignored. Just for script compatibility") + flags.String("default-stack-orchestrator", "", "Ignored. Just for script compatibility") } func add(cmd *cobra.Command, args []string) error { @@ -171,3 +197,59 @@ func add(cmd *cobra.Command, args []string) error { } return cfg.Write() } + +func create(cmd *cobra.Command, args []string) error { + dest, err := translateDest(dockerPath) + if err != nil { + return err + } + if match, err := regexp.Match("^[A-Za-z][A-Za-z0-9+.-]*://", []byte(dest)); err != nil { + return fmt.Errorf("invalid destination: %w", err) + } else if !match { + dest = "ssh://" + dest + } + + uri, err := url.Parse(dest) + if err != nil { + return err + } + + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + dst := config.Destination{ + URI: uri.String(), + } + + if cfg.Engine.ServiceDestinations == nil { + cfg.Engine.ServiceDestinations = map[string]config.Destination{ + args[0]: dst, + } + cfg.Engine.ActiveService = args[0] + } else { + cfg.Engine.ServiceDestinations[args[0]] = dst + } + return cfg.Write() +} + +func translateDest(path string) (string, error) { + if path == "" { + return "", nil + } + split := strings.SplitN(path, "=", 2) + if len(split) == 1 { + return split[0], nil + } + if split[0] != "host" { + return "", fmt.Errorf("\"host\" is requited for --docker option") + } + // "host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file" + vals := strings.Split(split[1], ",") + if len(vals) > 1 { + return "", fmt.Errorf("--docker additional options %q not supported", strings.Join(vals[1:], ",")) + } + // for now we ignore other fields specified on command line + return vals[0], nil +} diff --git a/cmd/podman/system/connection/default.go b/cmd/podman/system/connection/default.go index 81866df55f82..8d1709e9f402 100644 --- a/cmd/podman/system/connection/default.go +++ b/cmd/podman/system/connection/default.go @@ -21,9 +21,23 @@ var ( RunE: defaultRunE, Example: `podman system connection default testing`, } + + useCmd = &cobra.Command{ + Use: "use NAME", + Args: cobra.ExactArgs(1), + Short: dfltCmd.Short, + Long: dfltCmd.Long, + ValidArgsFunction: dfltCmd.ValidArgsFunction, + RunE: dfltCmd.RunE, + Example: `podman context use testing`, + } ) func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: useCmd, + Parent: system.ContextCmd, + }) registry.Commands = append(registry.Commands, registry.CliCommand{ Command: dfltCmd, Parent: system.ConnectionCmd, diff --git a/cmd/podman/system/connection/list.go b/cmd/podman/system/connection/list.go index 2c5f6a310bf7..190a68d529f2 100644 --- a/cmd/podman/system/connection/list.go +++ b/cmd/podman/system/connection/list.go @@ -8,6 +8,7 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/report" + "github.com/containers/common/pkg/util" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/system" @@ -29,16 +30,36 @@ var ( RunE: list, TraverseChildren: false, } + inspectCmd = &cobra.Command{ + Use: "inspect [options] [CONTEXT] [CONTEXT...]", + Short: "Inspect destination for a Podman service(s)", + ValidArgsFunction: completion.AutocompleteNone, + RunE: inspect, + } ) func init() { + initFlags := func(cmd *cobra.Command) { + cmd.Flags().StringP("format", "f", "", "Custom Go template for printing connections") + _ = cmd.RegisterFlagCompletionFunc("format", common.AutocompleteFormat(&namedDestination{})) + cmd.Flags().BoolP("quiet", "q", false, "Custom Go template for printing connections") + } + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: listCmd, + Parent: system.ContextCmd, + }) registry.Commands = append(registry.Commands, registry.CliCommand{ Command: listCmd, Parent: system.ConnectionCmd, }) + initFlags(listCmd) - listCmd.Flags().String("format", "", "Custom Go template for printing connections") - _ = listCmd.RegisterFlagCompletionFunc("format", common.AutocompleteFormat(&namedDestination{})) + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: inspectCmd, + Parent: system.ContextCmd, + }) + initFlags(inspectCmd) } type namedDestination struct { @@ -48,13 +69,34 @@ type namedDestination struct { } func list(cmd *cobra.Command, _ []string) error { + return inspect(cmd, nil) +} + +func inspect(cmd *cobra.Command, args []string) error { cfg, err := config.ReadCustomConfig() if err != nil { return err } + format := cmd.Flag("format").Value.String() + if format == "" && args != nil { + format = "json" + } + + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return err + } rows := make([]namedDestination, 0) for k, v := range cfg.Engine.ServiceDestinations { + if args != nil && !util.StringInSlice(k, args) { + continue + } + + if quiet { + fmt.Println(k) + continue + } def := false if k == cfg.Engine.ActiveService { def = true @@ -71,6 +113,10 @@ func list(cmd *cobra.Command, _ []string) error { rows = append(rows, r) } + if quiet { + return nil + } + sort.Slice(rows, func(i, j int) bool { return rows[i].Name < rows[j].Name }) @@ -78,7 +124,7 @@ func list(cmd *cobra.Command, _ []string) error { rpt := report.New(os.Stdout, cmd.Name()) defer rpt.Flush() - if report.IsJSON(cmd.Flag("format").Value.String()) { + if report.IsJSON(format) { buf, err := registry.JSONLibrary().MarshalIndent(rows, "", " ") if err == nil { fmt.Println(string(buf)) @@ -86,8 +132,8 @@ func list(cmd *cobra.Command, _ []string) error { return err } - if cmd.Flag("format").Changed { - rpt, err = rpt.Parse(report.OriginUser, cmd.Flag("format").Value.String()) + if format != "" { + rpt, err = rpt.Parse(report.OriginUser, format) } else { rpt, err = rpt.Parse(report.OriginPodman, "{{range .}}{{.Name}}\t{{.URI}}\t{{.Identity}}\t{{.Default}}\n{{end -}}") diff --git a/cmd/podman/system/connection/remove.go b/cmd/podman/system/connection/remove.go index 29bf98c43c1f..5ff0000d6160 100644 --- a/cmd/podman/system/connection/remove.go +++ b/cmd/podman/system/connection/remove.go @@ -29,6 +29,11 @@ var ( ) func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: rmCmd, + Parent: system.ContextCmd, + }) + registry.Commands = append(registry.Commands, registry.CliCommand{ Command: rmCmd, Parent: system.ConnectionCmd, @@ -36,6 +41,9 @@ func init() { flags := rmCmd.Flags() flags.BoolVarP(&rmOpts.All, "all", "a", false, "Remove all connections") + + flags.BoolP("force", "f", false, "Ignored: for Docker compatibility") + _ = flags.MarkHidden("force") } func rm(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/system/context.go b/cmd/podman/system/context.go new file mode 100644 index 000000000000..70cfb3bae4c5 --- /dev/null +++ b/cmd/podman/system/context.go @@ -0,0 +1,24 @@ +package system + +import ( + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/validate" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _system_ + ContextCmd = &cobra.Command{ + Use: "context", + Short: "Manage podman context", + Long: "Manage podman context", + RunE: validate.SubCommandExists, + Hidden: true, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: ContextCmd, + }) +} diff --git a/docs/source/markdown/podman-system-connection-list.1.md b/docs/source/markdown/podman-system-connection-list.1.md index 23784a319565..b05bb11b2152 100644 --- a/docs/source/markdown/podman-system-connection-list.1.md +++ b/docs/source/markdown/podman-system-connection-list.1.md @@ -13,7 +13,7 @@ List ssh destination(s) for podman service(s). ## OPTIONS -#### **--format**=*format* +#### **--format**, **-f**=*format* Change the default output format. This can be of a supported type like 'json' or a Go template. Valid placeholders for the Go template listed below: @@ -25,6 +25,10 @@ Valid placeholders for the Go template listed below: | .URI | URI to podman service. Valid schemes are ssh://[user@]*host*[:port]*Unix domain socket*[?secure=True], unix://*Unix domain socket*, and tcp://localhost[:*port*] | | .Default | Indicates whether connection is the default | +#### **--quiet**, **-q** + +Only show connection names + ## EXAMPLE ``` $ podman system connection list diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 3588313c63a1..fb22ed221158 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -235,6 +235,7 @@ can_use_shortcut () if (strcmp (argv[argc], "mount") == 0 || strcmp (argv[argc], "machine") == 0 + || strcmp (argv[argc], "context") == 0 || strcmp (argv[argc], "search") == 0 || (strcmp (argv[argc], "system") == 0 && argv[argc+1] && strcmp (argv[argc+1], "service") != 0)) { diff --git a/test/system/272-system-connection.bats b/test/system/272-system-connection.bats index e9e9a01ea574..d467dd1a7a83 100644 --- a/test/system/272-system-connection.bats +++ b/test/system/272-system-connection.bats @@ -56,8 +56,24 @@ function _run_podman_remote() { c1="c1_$(random_string 15)" c2="c2_$(random_string 15)" - run_podman system connection add $c1 tcp://localhost:12345 - run_podman system connection add --default $c2 tcp://localhost:54321 + run_podman system connection add $c1 tcp://localhost:12345 + run_podman context create --docker "host=tcp://localhost:54321" $c2 + run_podman system connection ls + is "$output" \ + ".*$c1[ ]\+tcp://localhost:12345[ ]\+true +$c2[ ]\+tcp://localhost:54321[ ]\+false" \ + "system connection ls" + run_podman system connection ls -q + is "$output" \ + "$c1 +$c2" \ + "system connection ls -q should show two names" + run_podman context ls -q + is "$output" \ + "$c1 +$c2" \ + "context ls -q should show two names" + run_podman context use $c2 run_podman system connection ls is "$output" \ ".*$c1[ ]\+tcp://localhost:12345[ ]\+false @@ -66,11 +82,14 @@ $c2[ ]\+tcp://localhost:54321[ ]\+true" \ # Remove default connection; the remaining one should still not be default run_podman system connection rm $c2 - run_podman system connection ls - is "$output" ".*$c1[ ]\+tcp://localhost:12345[ ]\+false" \ + run_podman context ls + is "$output" ".*$c1[ ]\+tcp://localhost:12345[ ]\+true" \ "system connection ls (after removing default connection)" + run_podman context inspect $c1 + is "$output" ".*$c1[ ]\+tcp://localhost:12345[ ]\+true" \ + "verify output from podman context inspect" - run_podman system connection rm $c1 + run_podman context rm $c1 } # Test tcp socket; requires starting a local server @@ -157,4 +176,38 @@ $c2[ ]\+tcp://localhost:54321[ ]\+true" \ run_podman system connection rm mysshconn } +# Very basic test, does not actually connect at any time +@test "podman system connection default" { + run_podman system connection ls + is "$output" "Name URI Identity Default" \ + "system connection ls: no connections" + + c1="c1_$(random_string 15)" + c2="c2_$(random_string 15)" + + run_podman system connection add $c1 tcp://localhost:12345 + run_podman context create --docker host=tcp://localhost:54321 $c2 + run_podman system connection ls + is "$output" ".*$c1[ ]\+tcp://localhost:12345[ ]\+true +$c2[ ]\+tcp://localhost:54321[ ]\+false" \ + run_podman context use $c2 + is "$output" \ + ".*$c1[ ]\+tcp://localhost:12345[ ]\+false +$c2[ ]\+tcp://localhost:54321[ ]\+true" \ + "system connection ls use c2" + run_podman system connection default $c1 + is "$output" \ + ".*$c1[ ]\+tcp://localhost:12345[ ]\+true +$c2[ ]\+tcp://localhost:54321[ ]\+false" \ + "system connection ls use c2" + + # Remove default connection; the remaining one should still not be default + run_podman system connection rm $c2 + run_podman context ls + is "$output" ".*$c1[ ]\+tcp://localhost:12345[ ]\+false" \ + "system connection ls (after removing default connection)" + + run_podman context rm $c1 +} + # vim: filetype=sh