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>
This commit is contained in:
Jhon Honce
2020-06-04 11:51:29 -07:00
parent b2c42a0993
commit 7377e578a9
14 changed files with 411 additions and 186 deletions

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -2,6 +2,13 @@
package registry
import (
"os"
)
func init() {
abiSupport = false
// Enforce that podman-remote == podman --remote
os.Args = append(os.Args, "--remote")
}

View File

@ -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\")")

View File

@ -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...)
}
return nil
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))
}
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
}

View File

@ -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)

View File

@ -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

View File

@ -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*

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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)

133
pkg/terminal/util.go Normal file
View File

@ -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
}