mirror of
https://github.com/containers/podman.git
synced 2025-10-13 17:26:13 +08:00
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:
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -2,6 +2,13 @@
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func init() {
|
||||
abiSupport = false
|
||||
|
||||
// Enforce that podman-remote == podman --remote
|
||||
os.Args = append(os.Args, "--remote")
|
||||
}
|
||||
|
@ -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\")")
|
||||
|
@ -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
|
||||
}
|
||||
|
37
docs/source/markdown/podman-system-connection.1.md
Normal file
37
docs/source/markdown/podman-system-connection.1.md
Normal 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)
|
@ -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
|
||||
|
@ -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*
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
133
pkg/terminal/util.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user