summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJhon Honce <jhonce@redhat.com>2020-06-04 11:51:29 -0700
committerJhon Honce <jhonce@redhat.com>2020-06-23 08:18:53 -0700
commit7377e578a9402a416579be92cee44c3b3786f81a (patch)
treec1424a43cc606ed11c8b0b5fb5f9f199ab0d83c5
parentb2c42a0993d2129c910036d4054ec25129d09f18 (diff)
downloadpodman-7377e578a9402a416579be92cee44c3b3786f81a.tar.gz
podman-7377e578a9402a416579be92cee44c3b3786f81a.tar.bz2
podman-7377e578a9402a416579be92cee44c3b3786f81a.zip
V2 podman system connection
* Implement command * Refactor podman-remote to pull from containers.conf by default * podman-remote defaults to --remote being true * Write podman-system-connection.1.md Signed-off-by: Jhon Honce <jhonce@redhat.com>
-rw-r--r--cmd/podman/main.go2
-rw-r--r--cmd/podman/registry/config.go3
-rw-r--r--cmd/podman/registry/config_tunnel.go7
-rw-r--r--cmd/podman/root.go17
-rw-r--r--cmd/podman/system/connection.go221
-rw-r--r--docs/source/markdown/podman-system-connection.1.md37
-rw-r--r--docs/source/markdown/podman-system.1.md19
-rw-r--r--docs/source/markdown/podman.1.md6
-rw-r--r--pkg/bindings/bindings.go43
-rw-r--r--pkg/bindings/connection.go98
-rw-r--r--pkg/domain/entities/engine.go3
-rw-r--r--pkg/domain/infra/runtime_abi.go4
-rw-r--r--pkg/domain/infra/runtime_tunnel.go4
-rw-r--r--pkg/terminal/util.go133
14 files changed, 411 insertions, 186 deletions
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index 76ec7bc8e..f502e7a67 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -35,7 +35,7 @@ func main() {
_, found := c.Command.Annotations[registry.ParentNSRequired]
if rootless.IsRootless() && found {
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
- return fmt.Errorf("cannot `%s` in rootless mode", cmd.CommandPath())
+ return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
}
}
diff --git a/cmd/podman/registry/config.go b/cmd/podman/registry/config.go
index 49d5bca74..a67568d73 100644
--- a/cmd/podman/registry/config.go
+++ b/cmd/podman/registry/config.go
@@ -68,7 +68,6 @@ func newPodmanConfig() {
}
}
- // FIXME: for rootless, add flag to get the path to override configuration
cfg, err := config.NewConfig("")
if err != nil {
fmt.Fprint(os.Stderr, "Failed to obtain podman configuration: "+err.Error())
@@ -83,7 +82,7 @@ func newPodmanConfig() {
podmanOptions = entities.PodmanConfig{Config: cfg, EngineMode: mode}
}
-// SetXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set.
+// setXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set.
// containers/image uses XDG_RUNTIME_DIR to locate the auth file, XDG_CONFIG_HOME is
// use for the libpod.conf configuration file.
func setXdgDirs() error {
diff --git a/cmd/podman/registry/config_tunnel.go b/cmd/podman/registry/config_tunnel.go
index bb3da947e..4f9f51163 100644
--- a/cmd/podman/registry/config_tunnel.go
+++ b/cmd/podman/registry/config_tunnel.go
@@ -2,6 +2,13 @@
package registry
+import (
+ "os"
+)
+
func init() {
abiSupport = false
+
+ // Enforce that podman-remote == podman --remote
+ os.Args = append(os.Args, "--remote")
}
diff --git a/cmd/podman/root.go b/cmd/podman/root.go
index 4f834e87d..25e53cbee 100644
--- a/cmd/podman/root.go
+++ b/cmd/podman/root.go
@@ -8,6 +8,7 @@ import (
"runtime/pprof"
"strings"
+ "github.com/containers/common/pkg/config"
"github.com/containers/libpod/cmd/podman/registry"
"github.com/containers/libpod/cmd/podman/validate"
"github.com/containers/libpod/pkg/domain/entities"
@@ -103,13 +104,13 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
// TODO: Remove trace statement in podman V2.1
logrus.Debugf("Called %s.PersistentPreRunE(%s)", cmd.Name(), strings.Join(os.Args, " "))
- cfg := registry.PodmanConfig()
-
// Help is a special case, no need for more setup
if cmd.Name() == "help" {
return nil
}
+ cfg := registry.PodmanConfig()
+
// Prep the engines
if _, err := registry.NewImageEngine(cmd, args); err != nil {
return err
@@ -211,10 +212,14 @@ func loggingHook() {
func rootFlags(opts *entities.PodmanConfig, flags *pflag.FlagSet) {
// V2 flags
flags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)")
- // TODO Read uri from containers.config when available
- flags.StringVar(&opts.URI, "url", registry.DefaultAPIAddress(), "URL to access Podman service (CONTAINER_HOST)")
- flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file, (CONTAINER_SSHKEY)")
- flags.StringVar(&opts.PassPhrase, "passphrase", "", "passphrase for identity file (not secure, CONTAINER_PASSPHRASE), ssh-agent always supported")
+
+ custom, _ := config.ReadCustomConfig()
+ defaultURI := custom.Engine.RemoteURI
+ if defaultURI == "" {
+ defaultURI = registry.DefaultAPIAddress()
+ }
+ flags.StringVar(&opts.URI, "url", defaultURI, "URL to access Podman service (CONTAINER_HOST)")
+ flags.StringVar(&opts.Identity, "identity", custom.Engine.RemoteIdentity, "path to SSH identity file, (CONTAINER_SSHKEY)")
cfg := opts.Config
flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")")
diff --git a/cmd/podman/system/connection.go b/cmd/podman/system/connection.go
index 3af905fad..d8c709d6e 100644
--- a/cmd/podman/system/connection.go
+++ b/cmd/podman/system/connection.go
@@ -1,54 +1,209 @@
-// +build !remote
-
package system
import (
+ "bytes"
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ "os/user"
+ "regexp"
+
+ "github.com/containers/common/pkg/config"
"github.com/containers/libpod/cmd/podman/registry"
+ "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/domain/entities"
+ "github.com/containers/libpod/pkg/terminal"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/agent"
)
+const schemaPattern = "^[A-Za-z][A-Za-z0-9+.-]*:"
+
var (
- connectionDescription = `TBD
-`
- connectionCommand = &cobra.Command{
- Use: "connection",
- //Args: validate.NoArgs,
- Long: connectionDescription,
- Short: "Add remote ssh connection",
- RunE: connection,
- Example: `podman system connection server.foobar.com
-podman system connection --identity ~/.ssh/dev_rsa --default root@server.foobar.com:222`,
+ // Skip creating engines since this command will obtain connection information to engine
+ noOp = func(cmd *cobra.Command, args []string) error {
+ return nil
+ }
+ connectionCmd = &cobra.Command{
+ Use: "connection [flags] destination",
+ Args: cobra.ExactArgs(1),
+ Long: `Store ssh destination information in podman configuration.
+ "destination" is of the form [user@]hostname or
+ an URI of the form ssh://[user@]hostname[:port]
+`,
+ Short: "Record remote ssh destination",
+ PersistentPreRunE: noOp,
+ PersistentPostRunE: noOp,
+ TraverseChildren: false,
+ RunE: connection,
+ Example: `podman system connection server.fubar.com
+ podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222
+ podman system connection --identity ~/.ssh/dev_rsa -port 22 root@server.fubar.com`,
}
-)
-var connectionOptions = struct {
- Alias string
- Default bool
- Identity string
- SocketPath string
- User string
-}{}
+ cOpts = struct {
+ Identity string
+ Port int
+ UDSPath string
+ }{}
+)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
- Mode: []entities.EngineMode{entities.ABIMode},
- Command: connectionCommand,
+ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+ Command: connectionCmd,
Parent: systemCmd,
})
- flags := connectionCommand.Flags()
- flags.StringVar(&connectionOptions.Alias, "alias", "", "alias name for connection")
- flags.BoolVar(&connectionOptions.Default, "default", false, "set as the default connection")
- flags.StringVar(&connectionOptions.Identity, "identity", "", "path to ssh identity file")
- //flags.StringVar(&connectionOptions.User, "user", "", "remote username")
- flags.StringVar(&connectionOptions.SocketPath, "socket-path", "", "path to podman socket on remote host")
+
+ flags := connectionCmd.Flags()
+ flags.StringVar(&cOpts.Identity, "identity", "", "path to ssh identity file")
+ flags.IntVarP(&cOpts.Port, "port", "p", 22, "port number for destination")
+ flags.StringVar(&cOpts.UDSPath, "socket-path", "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)")
}
func connection(cmd *cobra.Command, args []string) error {
- // if no user is provided, assume local user name
- // if no socket is provided, then do an ssh to look for it
- // default connection, if exists, is then assumed with podman remote
- // if no identity exists, should we be prompting for password?
+ // Default to ssh: schema if none given
+ dest := []byte(args[0])
+ if match, err := regexp.Match(schemaPattern, dest); err != nil {
+ return errors.Wrapf(err, "internal regex error %q", schemaPattern)
+ } else if !match {
+ dest = append([]byte("ssh://"), dest...)
+ }
+
+ uri, err := url.Parse(string(dest))
+ if err != nil {
+ return errors.Wrapf(err, "failed to parse %q", string(dest))
+ }
+
+ if uri.User.Username() == "" {
+ if uri.User, err = getUserInfo(uri); err != nil {
+ return err
+ }
+ }
+
+ if cmd.Flag("socket-path").Changed {
+ uri.Path = cmd.Flag("socket-path").Value.String()
+ }
+
+ if cmd.Flag("port").Changed {
+ uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").Value.String())
+ }
+
+ if uri.Port() == "" {
+ uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue)
+ }
+
+ if uri.Path == "" {
+ if uri.Path, err = getUDS(cmd, uri); err != nil {
+ return errors.Wrapf(err, "failed to connect to %q", uri.String())
+ }
+ }
+
+ custom, err := config.ReadCustomConfig()
+ if err != nil {
+ return err
+ }
+
+ if cmd.Flag("identity").Changed {
+ custom.Engine.RemoteIdentity = cOpts.Identity
+ }
+
+ custom.Engine.RemoteURI = uri.String()
+ return custom.Write()
+}
+
+func getUserInfo(uri *url.URL) (*url.Userinfo, error) {
+ var (
+ usr *user.User
+ err error
+ )
+ if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found {
+ usr, err = user.LookupId(u)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to find user %q", u)
+ }
+ } else {
+ usr, err = user.Current()
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to obtain current user")
+ }
+ }
+
+ pw, set := uri.User.Password()
+ if set {
+ return url.UserPassword(usr.Username, pw), nil
+ }
+ return url.User(usr.Username), nil
+}
+
+func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) {
+ var authMethods []ssh.AuthMethod
+ passwd, set := uri.User.Password()
+ if set {
+ authMethods = append(authMethods, ssh.Password(passwd))
+ }
+
+ ident := cmd.Flag("identity")
+ if ident.Changed {
+ auth, err := terminal.PublicKey(ident.Value.String(), []byte(passwd))
+ if err != nil {
+ return "", errors.Wrapf(err, "Failed to read identity %q", ident.Value.String())
+ }
+ authMethods = append(authMethods, auth)
+ }
+
+ if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found {
+ logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)
+
+ c, err := net.Dial("unix", sock)
+ if err != nil {
+ return "", err
+ }
+ a := agent.NewClient(c)
+ authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers))
+ }
- return nil
+ config := &ssh.ClientConfig{
+ User: uri.User.Username(),
+ Auth: authMethods,
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ }
+ dial, err := ssh.Dial("tcp", uri.Host, config)
+ if err != nil {
+ return "", errors.Wrapf(err, "failed to connect to %q", uri.Host)
+ }
+ defer dial.Close()
+
+ session, err := dial.NewSession()
+ if err != nil {
+ return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host)
+ }
+ defer session.Close()
+
+ // Override podman binary for testing etc
+ podman := "podman"
+ if v, found := os.LookupEnv("PODMAN_BINARY"); found {
+ podman = v
+ }
+ run := podman + " info --format=json"
+
+ var buffer bytes.Buffer
+ session.Stdout = &buffer
+ if err := session.Run(run); err != nil {
+ return "", errors.Wrapf(err, "failed to run %q", run)
+ }
+
+ var info define.Info
+ if err := json.Unmarshal(buffer.Bytes(), &info); err != nil {
+ return "", errors.Wrapf(err, "failed to parse 'podman info' results")
+ }
+
+ if info.Host.RemoteSocket == nil || !info.Host.RemoteSocket.Exists {
+ return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host)
+ }
+ return info.Host.RemoteSocket.Path, nil
}
diff --git a/docs/source/markdown/podman-system-connection.1.md b/docs/source/markdown/podman-system-connection.1.md
new file mode 100644
index 000000000..ed73980d6
--- /dev/null
+++ b/docs/source/markdown/podman-system-connection.1.md
@@ -0,0 +1,37 @@
+% podman-system-connection(1)
+
+## NAME
+podman\-system\-connection - Record ssh destination for remote podman service
+
+## SYNOPSIS
+**podman system connection** [*options*] [*ssh destination*]
+
+## DESCRIPTION
+Record ssh destination for remote podman service(s). The ssh destination is given as one of:
+ - [user@]hostname[:port]
+ - ssh://[user@]hostname[:port]
+
+The user will be prompted for the remote ssh login password or key file pass phrase as required. `ssh-agent` is supported if it is running.
+
+## OPTIONS
+
+**-p**, **--port**=*port*
+
+Port for ssh destination. The default value is `22`.
+
+**--socket-path**=*path*
+
+Path to podman service unix domain socket on the ssh destination host
+
+## EXAMPLE
+```
+$ podman system connection podman.fubar.com
+
+$ podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222
+
+```
+## SEE ALSO
+podman-system(1) , containers.conf(5) , connections.conf(5)
+
+## HISTORY
+June 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com)
diff --git a/docs/source/markdown/podman-system.1.md b/docs/source/markdown/podman-system.1.md
index 5f163c6f0..1f19fd0b6 100644
--- a/docs/source/markdown/podman-system.1.md
+++ b/docs/source/markdown/podman-system.1.md
@@ -11,15 +11,16 @@ The system command allows you to manage the podman systems
## COMMANDS
-| Command | Man Page | Description |
-| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
-| df | [podman-system-df(1)](podman-system-df.1.md) | Show podman disk usage. |
-| info | [podman-system-info(1)](podman-info.1.md) | Displays Podman related system information. |
-| migrate | [podman-system-migrate(1)](podman-system-migrate.1.md)| Migrate existing containers to a new podman version. |
-| prune | [podman-system-prune(1)](podman-system-prune.1.md) | Remove all unused container, image and volume data. |
-| renumber | [podman-system-renumber(1)](podman-system-renumber.1.md)| Migrate lock numbers to handle a change in maximum number of locks. |
-| reset | [podman-system-reset(1)](podman-system-reset.1.md) | Reset storage back to initial state. |
-| service | [podman-service(1)](podman-system-service.1.md) | Run an API service |
+| Command | Man Page | Description |
+| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
+| df | [podman-system-df(1)](podman-system-df.1.md) | Show podman disk usage. |
+| connection | [podman-system-connection(1)](podman-system-connection.1.md) | Record ssh destination for remote podman service. |
+| info | [podman-system-info(1)](podman-info.1.md) | Displays Podman related system information. |
+| migrate | [podman-system-migrate(1)](podman-system-migrate.1.md) | Migrate existing containers to a new podman version. |
+| prune | [podman-system-prune(1)](podman-system-prune.1.md) | Remove all unused container, image and volume data. |
+| renumber | [podman-system-renumber(1)](podman-system-renumber.1.md) | Migrate lock numbers to handle a change in maximum number of locks. |
+| reset | [podman-system-reset(1)](podman-system-reset.1.md) | Reset storage back to initial state. |
+| service | [podman-service(1)](podman-system-service.1.md) | Run an API service |
## SEE ALSO
diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md
index 2f338452c..ce02ef3a7 100644
--- a/docs/source/markdown/podman.1.md
+++ b/docs/source/markdown/podman.1.md
@@ -59,10 +59,10 @@ Podman and libpod currently support an additional `precreate` state which is cal
**WARNING**: the `precreate` hook lets you do powerful things, such as adding additional mounts to the runtime configuration. That power also makes it easy to break things. Before reporting libpod errors, try running your container with `precreate` hooks disabled to see if the problem is due to one of your hooks.
**--identity**=*path*
-Path to SSH identity file
-**--passphrase**=*secret*
-pass phrase for SSH identity file
+Path to ssh identity file. If the identity file has been encrypted, podman prompts the user for the passphrase.
+If no identity file is provided and no user is given, podman defaults to the user running the podman command.
+Podman prompts for the login password on the remote server.
**--log-level**=*level*
diff --git a/pkg/bindings/bindings.go b/pkg/bindings/bindings.go
index 94f7a45d0..ae5610b0f 100644
--- a/pkg/bindings/bindings.go
+++ b/pkg/bindings/bindings.go
@@ -8,13 +8,7 @@
package bindings
import (
- "errors"
- "fmt"
- "io"
- "os"
-
"github.com/blang/semver"
- "golang.org/x/crypto/ssh/terminal"
)
var (
@@ -30,40 +24,3 @@ var (
// APIVersion - podman will fail to run if this value is wrong
APIVersion = semver.MustParse("1.0.0")
)
-
-// readPassword prompts for a secret and returns value input by user from stdin
-// Unlike terminal.ReadPassword(), $(echo $SECRET | podman...) is supported.
-// Additionally, all input after `<secret>/n` is queued to podman command.
-func readPassword(prompt string) (pw []byte, err error) {
- fd := int(os.Stdin.Fd())
- if terminal.IsTerminal(fd) {
- fmt.Fprint(os.Stderr, prompt)
- pw, err = terminal.ReadPassword(fd)
- fmt.Fprintln(os.Stderr)
- return
- }
-
- var b [1]byte
- for {
- n, err := os.Stdin.Read(b[:])
- // terminal.ReadPassword discards any '\r', so we do the same
- if n > 0 && b[0] != '\r' {
- if b[0] == '\n' {
- return pw, nil
- }
- pw = append(pw, b[0])
- // limit size, so that a wrong input won't fill up the memory
- if len(pw) > 1024 {
- err = errors.New("password too long, 1024 byte limit")
- }
- }
- if err != nil {
- // terminal.ReadPassword accepts EOF-terminated passwords
- // if non-empty, so we do the same
- if err == io.EOF && len(pw) > 0 {
- err = nil
- }
- return pw, err
- }
- }
-}
diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go
index a9c61e5ae..584aa55c1 100644
--- a/pkg/bindings/connection.go
+++ b/pkg/bindings/connection.go
@@ -1,28 +1,24 @@
package bindings
import (
- "bufio"
"context"
"fmt"
"io"
- "io/ioutil"
"net"
"net/http"
"net/url"
"os"
- "path/filepath"
"strconv"
"strings"
- "sync"
"time"
"github.com/blang/semver"
+ "github.com/containers/libpod/pkg/terminal"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
- "k8s.io/client-go/util/homedir"
)
var (
@@ -31,8 +27,6 @@ var (
Host: "d",
Path: "/v" + APIVersion.String() + "/libpod",
}
- passPhrase []byte
- phraseSync sync.Once
)
type APIResponse struct {
@@ -77,7 +71,7 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) {
// For example tcp://localhost:<port>
// or unix:///run/podman/podman.sock
// or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True
-func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase string, identities ...string) (context.Context, error) {
+func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) {
var (
err error
secure bool
@@ -86,11 +80,12 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin
uri = v
}
- if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identities) == 0 {
- identities = append(identities, v)
+ if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identity) == 0 {
+ identity = v
}
- if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found && passPhrase == "" {
+ passPhrase := ""
+ if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found {
passPhrase = v
}
@@ -98,7 +93,6 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin
if err != nil {
return nil, errors.Wrapf(err, "Value of CONTAINER_HOST is not a valid url: %s", uri)
}
- // TODO Fill in missing defaults for _url...
// Now we setup the http Client to use the connection above
var connection Connection
@@ -108,7 +102,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin
if err != nil {
secure = false
}
- connection, err = sshClient(_url, secure, passPhrase, identities...)
+ connection, err = sshClient(_url, secure, passPhrase, identity)
case "unix":
if !strings.HasPrefix(uri, "unix:///") {
// autofix unix://path_element vs unix:///path_element
@@ -122,7 +116,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin
}
connection = tcpClient(_url)
default:
- return nil, errors.Errorf("'%s' is not a supported schema", _url.Scheme)
+ return nil, errors.Errorf("unable to create connection. %q is not a supported schema", _url.Scheme)
}
if err != nil {
return nil, errors.Wrapf(err, "Failed to create %sClient", _url.Scheme)
@@ -185,16 +179,14 @@ func pingNewConnection(ctx context.Context) error {
return errors.Errorf("ping response was %q", response.StatusCode)
}
-func sshClient(_url *url.URL, secure bool, passPhrase string, identities ...string) (Connection, error) {
+func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) {
authMethods := []ssh.AuthMethod{}
- for _, i := range identities {
- auth, err := publicKey(i, []byte(passPhrase))
- if err != nil {
- fmt.Fprint(os.Stderr, errors.Wrapf(err, "failed to parse identity %q", i).Error()+"\n")
- continue
- }
- authMethods = append(authMethods, auth)
+ auth, err := terminal.PublicKey(identity, []byte(passPhrase))
+ if err != nil {
+ return Connection{}, errors.Wrapf(err, "failed to parse identity %q", identity)
}
+ logrus.Debugf("public key signer enabled for identity %q", identity)
+ authMethods = append(authMethods, auth)
if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found {
logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)
@@ -213,7 +205,7 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identities ...stri
callback := ssh.InsecureIgnoreHostKey()
if secure {
- key := hostKey(_url.Hostname())
+ key := terminal.HostKey(_url.Hostname())
if key != nil {
callback = ssh.FixedHostKey(key)
}
@@ -339,63 +331,3 @@ func (h *APIResponse) IsClientError() bool {
func (h *APIResponse) IsServerError() bool {
return h.Response.StatusCode/100 == 5
}
-
-func publicKey(path string, passphrase []byte) (ssh.AuthMethod, error) {
- key, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, err
- }
-
- signer, err := ssh.ParsePrivateKey(key)
- if err != nil {
- if _, ok := err.(*ssh.PassphraseMissingError); !ok {
- return nil, err
- }
- if len(passphrase) == 0 {
- phraseSync.Do(promptPassphrase)
- passphrase = passPhrase
- }
- signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphrase)
- if err != nil {
- return nil, err
- }
- }
- return ssh.PublicKeys(signer), nil
-}
-
-func promptPassphrase() {
- phrase, err := readPassword("Key Passphrase: ")
- if err != nil {
- passPhrase = []byte{}
- return
- }
- passPhrase = phrase
-}
-
-func hostKey(host string) ssh.PublicKey {
- // parse OpenSSH known_hosts file
- // ssh or use ssh-keyscan to get initial key
- knownHosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts")
- fd, err := os.Open(knownHosts)
- if err != nil {
- logrus.Error(err)
- return nil
- }
-
- scanner := bufio.NewScanner(fd)
- for scanner.Scan() {
- _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
- if err != nil {
- logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text())
- continue
- }
-
- for _, h := range hosts {
- if h == host {
- return key
- }
- }
- }
-
- return nil
-}
diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go
index 1f056bad7..6776d09e9 100644
--- a/pkg/domain/entities/engine.go
+++ b/pkg/domain/entities/engine.go
@@ -41,9 +41,8 @@ type PodmanConfig struct {
ConmonPath string // --conmon flag will set Engine.ConmonPath
CPUProfile string // Hidden: Should CPU profile be taken
EngineMode EngineMode // ABI or Tunneling mode
- Identities []string // ssh identities for connecting to server
+ Identity string // ssh identity for connecting to server
MaxWorks int // maximum number of parallel threads
- PassPhrase string // ssh passphrase for identity for connecting to server
RegistriesConf string // allows for specifying a custom registries.conf
Remote bool // Connection to Podman API Service will use RESTful API
RuntimePath string // --runtime flag will set Engine.RuntimePath
diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go
index 60d0c6e86..3b344cb08 100644
--- a/pkg/domain/infra/runtime_abi.go
+++ b/pkg/domain/infra/runtime_abi.go
@@ -20,7 +20,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine,
r, err := NewLibpodRuntime(facts.FlagSet, facts)
return r, err
case entities.TunnelMode:
- ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...)
+ ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity)
return &tunnel.ContainerEngine{ClientCxt: ctx}, err
}
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
@@ -33,7 +33,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error)
r, err := NewLibpodImageRuntime(facts.FlagSet, facts)
return r, err
case entities.TunnelMode:
- ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...)
+ ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity)
return &tunnel.ImageEngine{ClientCxt: ctx}, err
}
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go
index 24a93b888..039a8339b 100644
--- a/pkg/domain/infra/runtime_tunnel.go
+++ b/pkg/domain/infra/runtime_tunnel.go
@@ -16,7 +16,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine,
case entities.ABIMode:
return nil, fmt.Errorf("direct runtime not supported")
case entities.TunnelMode:
- ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...)
+ ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity)
return &tunnel.ContainerEngine{ClientCxt: ctx}, err
}
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
@@ -28,7 +28,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error)
case entities.ABIMode:
return nil, fmt.Errorf("direct image runtime not supported")
case entities.TunnelMode:
- ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...)
+ ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity)
return &tunnel.ImageEngine{ClientCxt: ctx}, err
}
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
diff --git a/pkg/terminal/util.go b/pkg/terminal/util.go
new file mode 100644
index 000000000..ab3dc54e4
--- /dev/null
+++ b/pkg/terminal/util.go
@@ -0,0 +1,133 @@
+package terminal
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/terminal"
+ "k8s.io/client-go/util/homedir"
+)
+
+var (
+ passPhrase []byte
+ phraseSync sync.Once
+ password []byte
+ passwordSync sync.Once
+)
+
+// ReadPassword prompts for a secret and returns value input by user from stdin
+// Unlike terminal.ReadPassword(), $(echo $SECRET | podman...) is supported.
+// Additionally, all input after `<secret>/n` is queued to podman command.
+func ReadPassword(prompt string) (pw []byte, err error) {
+ fd := int(os.Stdin.Fd())
+ if terminal.IsTerminal(fd) {
+ fmt.Fprint(os.Stderr, prompt)
+ pw, err = terminal.ReadPassword(fd)
+ fmt.Fprintln(os.Stderr)
+ return
+ }
+
+ var b [1]byte
+ for {
+ n, err := os.Stdin.Read(b[:])
+ // terminal.ReadPassword discards any '\r', so we do the same
+ if n > 0 && b[0] != '\r' {
+ if b[0] == '\n' {
+ return pw, nil
+ }
+ pw = append(pw, b[0])
+ // limit size, so that a wrong input won't fill up the memory
+ if len(pw) > 1024 {
+ err = errors.New("password too long, 1024 byte limit")
+ }
+ }
+ if err != nil {
+ // terminal.ReadPassword accepts EOF-terminated passwords
+ // if non-empty, so we do the same
+ if err == io.EOF && len(pw) > 0 {
+ err = nil
+ }
+ return pw, err
+ }
+ }
+}
+
+func PublicKey(path string, passphrase []byte) (ssh.AuthMethod, error) {
+ key, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ signer, err := ssh.ParsePrivateKey(key)
+ if err != nil {
+ if _, ok := err.(*ssh.PassphraseMissingError); !ok {
+ return nil, err
+ }
+ if len(passphrase) == 0 {
+ passphrase = ReadPassphrase()
+ }
+ signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphrase)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return ssh.PublicKeys(signer), nil
+}
+
+func ReadPassphrase() []byte {
+ phraseSync.Do(func() {
+ secret, err := ReadPassword("Key Passphrase: ")
+ if err != nil {
+ secret = []byte{}
+ }
+ passPhrase = secret
+ })
+ return passPhrase
+}
+
+func ReadLogin() []byte {
+ passwordSync.Do(func() {
+ secret, err := ReadPassword("Login password: ")
+ if err != nil {
+ secret = []byte{}
+ }
+ password = secret
+ })
+ return password
+}
+
+func HostKey(host string) ssh.PublicKey {
+ // parse OpenSSH known_hosts file
+ // ssh or use ssh-keyscan to get initial key
+ knownHosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts")
+ fd, err := os.Open(knownHosts)
+ if err != nil {
+ logrus.Error(err)
+ return nil
+ }
+
+ scanner := bufio.NewScanner(fd)
+ for scanner.Scan() {
+ _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
+ if err != nil {
+ logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text())
+ continue
+ }
+
+ for _, h := range hosts {
+ if h == host {
+ return key
+ }
+ }
+ }
+
+ return nil
+}