From 3508bd22fe355630453d497d905e480974a84b96 Mon Sep 17 00:00:00 2001
From: Daniel J Walsh <dwalsh@redhat.com>
Date: Fri, 9 Sep 2022 09:06:18 -0400
Subject: Add support for podman context as alias to podman system connection

Alias
podman --context -> 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 <dwalsh@redhat.com>
---
 cmd/podman/registry/remote.go           |  4 +-
 cmd/podman/root.go                      | 35 ++++++++++----
 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            | 28 +++++++++++
 7 files changed, 212 insertions(+), 17 deletions(-)
 create mode 100644 cmd/podman/system/context.go

(limited to 'cmd')

diff --git a/cmd/podman/registry/remote.go b/cmd/podman/registry/remote.go
index afe32e0b9..02aa31c58 100644
--- a/cmd/podman/registry/remote.go
+++ b/cmd/podman/registry/remote.go
@@ -32,6 +32,8 @@ func IsRemote() bool {
 		fs.BoolVarP(&remoteFromCLI.Value, "remote", "r", remote, "")
 		connectionFlagName := "connection"
 		fs.StringP(connectionFlagName, "c", "", "")
+		contextFlagName := "context"
+		fs.String(contextFlagName, "", "")
 		hostFlagName := "host"
 		fs.StringP(hostFlagName, "H", "", "")
 		urlFlagName := "url"
@@ -46,7 +48,7 @@ func IsRemote() bool {
 		}
 		_ = fs.Parse(os.Args[start:])
 		// --connection or --url implies --remote
-		remoteFromCLI.Value = remoteFromCLI.Value || fs.Changed(connectionFlagName) || fs.Changed(urlFlagName) || fs.Changed(hostFlagName)
+		remoteFromCLI.Value = remoteFromCLI.Value || fs.Changed(connectionFlagName) || fs.Changed(urlFlagName) || fs.Changed(hostFlagName) || fs.Changed(contextFlagName)
 	})
 	return podmanOptions.EngineMode == entities.TunnelMode || remoteFromCLI.Value
 }
diff --git a/cmd/podman/root.go b/cmd/podman/root.go
index 2e00777a4..9e3ff48aa 100644
--- a/cmd/podman/root.go
+++ b/cmd/podman/root.go
@@ -174,11 +174,7 @@ 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 {
-		cfg.Engine.ActiveService = conn.Value.String()
-
+	setupConnection := func() error {
 		var err error
 		cfg.URI, cfg.Identity, err = cfg.ActiveDestination()
 		if err != nil {
@@ -192,6 +188,29 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
 		if err := cmd.Root().LocalFlags().Set("identity", cfg.Identity); err != nil {
 			return fmt.Errorf("failed to override --identity flag: %w", err)
 		}
+		return nil
+	}
+
+	// --connection is not as "special" as --remote so we can wait and process it here
+	contextConn := cmd.Root().LocalFlags().Lookup("context")
+	conn := cmd.Root().LocalFlags().Lookup("connection")
+	if conn != nil && conn.Changed {
+		if contextConn != nil && contextConn.Changed {
+			return fmt.Errorf("use of --connection and --context at the same time is not allowed")
+		}
+		cfg.Engine.ActiveService = conn.Value.String()
+		if err := setupConnection(); err != nil {
+			return err
+		}
+	}
+	if contextConn != nil && contextConn.Changed {
+		service := contextConn.Value.String()
+		if service != "default" {
+			cfg.Engine.ActiveService = service
+			if err := setupConnection(); err != nil {
+				return err
+			}
+		}
 	}
 
 	// Special case if command is hidden completion command ("__complete","__completeNoDesc")
@@ -232,10 +251,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)
@@ -362,7 +377,7 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) {
 	_ = cmd.RegisterFlagCompletionFunc(sshFlagName, common.AutocompleteSSH)
 
 	connectionFlagName := "connection"
-	lFlags.StringVarP(&opts.Engine.ActiveService, connectionFlagName, "c", srv, "Connection to use for remote Podman service")
+	lFlags.StringP(connectionFlagName, "c", srv, "Connection to use for remote Podman service")
 	_ = cmd.RegisterFlagCompletionFunc(connectionFlagName, common.AutocompleteSystemConnections)
 
 	urlFlagName := "url"
diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go
index f3b61b254..2730ebfb7 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 81866df55..8d1709e9f 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 2c5f6a310..190a68d52 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 29bf98c43..5ff0000d6 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 000000000..926e4a443
--- /dev/null
+++ b/cmd/podman/system/context.go
@@ -0,0 +1,28 @@
+package system
+
+import (
+	"github.com/containers/podman/v4/cmd/podman/registry"
+	"github.com/containers/podman/v4/cmd/podman/validate"
+	"github.com/spf13/cobra"
+)
+
+var (
+	// ContextCmd skips creating engines (PersistentPreRunE/PersistentPostRunE are No-Op's) since
+	// sub-commands will obtain connection information to said engines
+	ContextCmd = &cobra.Command{
+		Use:                "context",
+		Short:              "Manage remote API service destinations",
+		Long:               `Manage remote API service destination information in podman configuration`,
+		PersistentPreRunE:  validate.NoOp,
+		RunE:               validate.SubCommandExists,
+		PersistentPostRunE: validate.NoOp,
+		Hidden:             true,
+		TraverseChildren:   false,
+	}
+)
+
+func init() {
+	registry.Commands = append(registry.Commands, registry.CliCommand{
+		Command: ContextCmd,
+	})
+}
-- 
cgit v1.2.3-54-g00ecf