Skip to content

Commit

Permalink
support SSH connection
Browse files Browse the repository at this point in the history
e.g. docker -H ssh://me@server

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Jul 27, 2018
1 parent 6011316 commit e6fc199
Show file tree
Hide file tree
Showing 19 changed files with 471 additions and 36 deletions.
43 changes: 33 additions & 10 deletions cli/command/cli.go
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/connhelper"
cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
Expand Down Expand Up @@ -248,31 +249,54 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerC

// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
unparsedHost, err := getUnparsedServerHost(opts.Hosts)
if err != nil {
return &client.Client{}, err
}
var clientOpts []func(*client.Client) error
helper, err := connhelper.GetConnectionHelper(unparsedHost)
if err != nil {
return &client.Client{}, err
}
if helper == nil {
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost)
if err != nil {
return &client.Client{}, err
}
clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
}

customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))

verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
verStr = tmpStr
}
clientOpts = append(clientOpts, client.WithVersion(verStr))

return client.NewClientWithOpts(
withHTTPClient(opts.TLSOptions),
client.WithHTTPHeaders(customHeaders),
client.WithVersion(verStr),
client.WithHost(host),
)
return client.NewClientWithOpts(clientOpts...)
}

func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
func getUnparsedServerHost(hosts []string) (string, error) {
var host string
switch len(hosts) {
case 0:
Expand All @@ -282,8 +306,7 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
default:
return "", errors.New("Please specify only one -H")
}

return dopts.ParseHost(tlsOptions != nil, host)
return host, nil
}

func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
Expand Down
8 changes: 8 additions & 0 deletions cli/command/out.go
Expand Up @@ -19,6 +19,14 @@ func (o *OutStream) Write(p []byte) (int, error) {
return o.out.Write(p)
}

// Close calls o.out.Close() if available.
func (o *OutStream) Close() error {
if wc, ok := o.out.(io.Closer); ok {
return wc.Close()
}
return nil
}

// SetRawTerminal sets raw mode on the input terminal
func (o *OutStream) SetRawTerminal() (err error) {
if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal {
Expand Down
1 change: 1 addition & 0 deletions cli/command/system/cmd.go
Expand Up @@ -19,6 +19,7 @@ func NewSystemCommand(dockerCli command.Cli) *cobra.Command {
NewInfoCommand(dockerCli),
newDiskUsageCommand(dockerCli),
newPruneCommand(dockerCli),
newDialStdioCommand(dockerCli),
)

return cmd
Expand Down
100 changes: 100 additions & 0 deletions cli/command/system/dial_stdio.go
@@ -0,0 +1,100 @@
package system

import (
"context"
"io"
"os"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)

type dialStdioOptions struct {
}

// newDialStdioCommand creates a new cobra.Command for `docker system dial-stdio`
func newDialStdioCommand(dockerCli command.Cli) *cobra.Command {
var opts dialStdioOptions

cmd := &cobra.Command{
Use: "dial-stdio [OPTIONS]",
Short: "Proxy the stdio stream to the daemon connection. Should not be invoked manually.",
Args: cli.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDialStdio(dockerCli, opts)
},
}
return cmd
}

func runDialStdio(dockerCli command.Cli, opts dialStdioOptions) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dialer := dockerCli.Client().Dialer()
conn, err := dialer(ctx)
if err != nil {
return errors.Wrap(err, "failed to open the raw stream connection")
}
connHalfCloser, ok := conn.(halfCloser)
if !ok {
return errors.New("the raw stream connection does not implement halfCloser")
}
stdio := &stdioHalfCloser{r: os.Stdin, w: os.Stdout}
var eg errgroup.Group
eg.Go(func() error { return copier(connHalfCloser, stdio) })
eg.Go(func() error { return copier(stdio, connHalfCloser) })
return eg.Wait()
}

func copier(to halfCloser, from halfCloser) error {
if _, err := io.Copy(to, from); err != nil {
return errors.Wrapf(err, "error while Copy (to=%+v, from=%+v)", to, from)
}
if err := from.CloseRead(); err != nil {
return errors.Wrapf(err, "error while CloseRead (from=%+v)", from)
}
if err := to.CloseWrite(); err != nil {
return errors.Wrapf(err, "error while CloseWrite (to=%+v)", to)
}
return nil
}

type halfReadCloser interface {
io.Reader
CloseRead() error
}

type halfWriteCloser interface {
io.Writer
CloseWrite() error
}

type halfCloser interface {
halfReadCloser
halfWriteCloser
}

type stdioHalfCloser struct {
r io.ReadCloser
w io.WriteCloser
}

func (x *stdioHalfCloser) Read(p []byte) (int, error) {
return x.r.Read(p)
}

func (x *stdioHalfCloser) CloseRead() error {
return x.r.Close()
}

func (x *stdioHalfCloser) Write(p []byte) (int, error) {
return x.w.Write(p)
}

func (x *stdioHalfCloser) CloseWrite() error {
return x.w.Close()
}
144 changes: 144 additions & 0 deletions cli/connhelper/connhelper.go
@@ -0,0 +1,144 @@
// Package connhelper provides helpers for connecting to a remote daemon host with custom logic.
package connhelper

import (
"context"
"fmt"
"io"
"net"
"net/url"
"os"
"os/exec"
"time"

"github.com/sirupsen/logrus"
)

// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
type ConnectionHelper struct {
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
Host string // dummy URL used for HTTP requests. e.g. "http://docker"
}

// GetConnectionHelper returns Docker-specific connection helper for the given URL.
// GetConnectionHelper returns nil without error when no helper is registered for the scheme.
// URL is like "ssh://me@server01".
func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) {
u, err := url.Parse(daemonURL)
if err != nil {
return nil, err
}
switch scheme := u.Scheme; scheme {
case "ssh":
return newSSHConnectionHelper(daemonURL)
}
// Future version may support plugins via ~/.docker/config.json. e.g. "dind"
// See docker/cli#889 for the previous discussion.
return nil, err
}

func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
var (
c commandConn
err error
)
c.cmd = exec.CommandContext(ctx, cmd, args...)
// we assume that args never contains sensitive information
logrus.Debugf("connhelper (%s): starting with %v", cmd, args)
c.cmd.Env = os.Environ()
setPdeathsig(c.cmd)
c.stdin, err = c.cmd.StdinPipe()
if err != nil {
return nil, err
}
c.stdout, err = c.cmd.StdoutPipe()
if err != nil {
return nil, err
}
c.cmd.Stderr = &logrusDebugWriter{
prefix: fmt.Sprintf("connhelper (%s):", cmd),
}
c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"}
c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"}
return &c, c.cmd.Start()
}

// commandConn implements net.Conn
type commandConn struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
localAddr net.Addr
remoteAddr net.Addr
}

func (c *commandConn) CloseRead() error {
return c.stdout.Close()
}

func (c *commandConn) Read(p []byte) (int, error) {
return c.stdout.Read(p)
}

func (c *commandConn) CloseWrite() error {
return c.stdin.Close()
}

func (c *commandConn) Write(p []byte) (int, error) {
return c.stdin.Write(p)
}

func (c *commandConn) Close() error {
if err := c.cmd.Process.Kill(); err != nil {
return err
}
_, werr := c.cmd.Process.Wait()
if err := c.stdin.Close(); err != nil {
logrus.Warnf("error while closing stdin: %v", err)
}
if err := c.stdout.Close(); err != nil {
logrus.Warnf("error while closing stdout: %v", err)
}
return werr
}

func (c *commandConn) LocalAddr() net.Addr {
return c.localAddr
}
func (c *commandConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
func (c *commandConn) SetDeadline(t time.Time) error {
logrus.Debugf("unimplemented call: SetDeadline(%v)", t)
return nil
}
func (c *commandConn) SetReadDeadline(t time.Time) error {
logrus.Debugf("unimplemented call: SetReadDeadline(%v)", t)
return nil
}
func (c *commandConn) SetWriteDeadline(t time.Time) error {
logrus.Debugf("unimplemented call: SetWriteDeadline(%v)", t)
return nil
}

type dummyAddr struct {
network string
s string
}

func (d dummyAddr) Network() string {
return d.network
}

func (d dummyAddr) String() string {
return d.s
}

type logrusDebugWriter struct {
prefix string
}

func (w *logrusDebugWriter) Write(p []byte) (int, error) {
logrus.Debugf("%s%s", w.prefix, string(p))
return len(p), nil
}
12 changes: 12 additions & 0 deletions cli/connhelper/connhelper_linux.go
@@ -0,0 +1,12 @@
package connhelper

import (
"os/exec"
"syscall"
)

func setPdeathsig(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGKILL,
}
}
10 changes: 10 additions & 0 deletions cli/connhelper/connhelper_nolinux.go
@@ -0,0 +1,10 @@
// +build !linux

package connhelper

import (
"os/exec"
)

func setPdeathsig(cmd *exec.Cmd) {
}

0 comments on commit e6fc199

Please sign in to comment.