Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support connection helpers (e.g. docker -H ssh://me@server) #889

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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/config/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 @@ -251,31 +252,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, configFile.ConnectionHelpers, "docker-connection-")
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.DummyHost))
clientOpts = append(clientOpts, client.WithHijackDialer(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 @@ -285,8 +309,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
1 change: 1 addition & 0 deletions cli/config/configfile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type ConfigFile struct {
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
Orchestrator string `json:"orchestrator,omitempty"`
ConnectionHelpers map[string]string `json:"connHelpers,omitempty"`
}

// ProxyConfig contains proxy configuration settings
Expand Down
159 changes: 159 additions & 0 deletions cli/config/connhelper/connhelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Package connhelper provides connection helpers.
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
//
// convention:
// * filename MUST be `docker-connection-%s`
// * called with args: {"connect", url}
// * stderr can be used for logging purpose
package connhelper

import (
"context"
"io"
"net"
"net/url"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/pkg/errors"
"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)
DummyHost string // dummy URL used for HTTP requests. e.g. "http://docker"
}

// GetConnectionHelper returns nil without error when no helper is registered for the scheme.
// host is like "ssh://me@server01:/var/run/docker.sock".
// cfg is like {"ssh": "ssh"}.
// prefix is like "docker-connection-".
func GetConnectionHelper(host string, cfg map[string]string, prefix string) (*ConnectionHelper, error) {
path, err := lookupConnectionHelperPath(host, cfg, prefix)
if path == "" || err != nil {
return nil, err
}
dialer := func(ctx context.Context, network, addr string) (net.Conn, error) {
return newHelperConn(ctx, path, host, network, addr)
}
return &ConnectionHelper{
Dialer: dialer,
DummyHost: "tcp://docker",
}, nil
}

// lookupConnectionHelperPath returns an empty string without error when no helper is registered for the scheme.
func lookupConnectionHelperPath(host string, cfg map[string]string, prefix string) (string, error) {
u, err := url.Parse(host)
if err != nil {
return "", err
}
if u.Scheme == "" {
return "", nil // unregistered
}
helperName := cfg[u.Scheme]
if helperName == "" {
return "", nil // unregistered
}
if strings.Contains(helperName, string(filepath.Separator)) {
return "", errors.Errorf("helper name (e.g. \"ssh\") should not contain path separator: %s", helperName)
}
return exec.LookPath(prefix + helperName)
}

func newHelperConn(ctx context.Context, helper string, host, dialNetwork, dialAddr string) (net.Conn, error) {
var (
c helperConn
err error
)
c.cmd = exec.CommandContext(ctx, helper, "connect", host)
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: "helper: ",
}
c.localAddr = dummyAddr{network: dialNetwork, s: "localhost"}
c.remoteAddr = dummyAddr{network: dialNetwork, s: dialAddr}
return &c, c.cmd.Start()
}

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

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

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

func (c *helperConn) Close() error {
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)
}
if err := c.cmd.Process.Kill(); err != nil {
return err
}
_, err := c.cmd.Process.Wait()
return err
}

func (c *helperConn) LocalAddr() net.Addr {
return c.localAddr
}
func (c *helperConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
func (c *helperConn) SetDeadline(t time.Time) error {
logrus.Debugf("unimplemented call: SetDeadline(%v)", t)
return nil
}
func (c *helperConn) SetReadDeadline(t time.Time) error {
logrus.Debugf("unimplemented call: SetReadDeadline(%v)", t)
return nil
}
func (c *helperConn) 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
}
3 changes: 2 additions & 1 deletion cli/flags/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) {
flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file")
flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file")

hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, opts.ValidateHost)
// opts.ValidateHost is not used here, so as to allow connection helpers
hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, nil)
flags.VarP(hostOpt, "host", "H", "Daemon socket(s) to connect to")
}

Expand Down
34 changes: 34 additions & 0 deletions contrib/connhelpers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Connection Helpers

Connection helpers allow connecting to a remote daemon with custom connection method.

## Installation

You need to put the following to `~/.docker/config.json`:
```
{
"connHelpers": {
"ssh": "ssh",
"dind": "dind"
}
}
```

## docker-connection-ssh

Usage:
```
$ docker -H ssh://[user@]host[:port][socketpath]
```

Requirements:

- public key authentication is configured
- `socat` must be installed on the remote host

## docker-connection-dind

Usage:
```
$ docker -H dind://containername
```
57 changes: 57 additions & 0 deletions contrib/connhelpers/docker-connection-dind/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"fmt"
"net/url"
"os"
"os/exec"
"strings"

"github.com/pkg/errors"
)

func main() {
if err := xmain(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func xmain() error {
if len(os.Args) != 3 || os.Args[1] != "connect" {
return errors.Errorf("usage: %s connect URL", os.Args[0])
}
u, err := url.Parse(os.Args[2])
if err != nil {
return err
}
if u.Scheme != "dind" {
return errors.Errorf("expected scheme: dind, got %s", u.Scheme)
}
// TODO: support setting realDockerHost and realDockerTLSVerify
// dind://containername?host=tcp%3A%2F%2Fhost%3A2376?tlsVerify=1
return execDockerExec(u.Hostname(), "", false)
}

func execDockerExec(containerName, realDockerHost string, realDockerTLSVerify bool) error {
// Astonishngly we can't use nc, as nc does not exit when the remote connection is closed.
// cmd := exec.Command("docker", "exec", "-i", containerName, "nc", "localhost", "2375")
cmd := exec.Command("docker", "exec", "-i", containerName,
"docker", "run", "-i", "--rm", "-v", "/var/run/docker.sock:/var/run/docker.sock", "alpine/socat:1.0.1", "unix:/var/run/docker.sock", "stdio")
cmd.Env = os.Environ()
for i, s := range cmd.Env {
if strings.HasPrefix(s, "DOCKER_HOST=") || strings.HasPrefix(s, "DOCKER_TLS_VERIFY=") {
cmd.Env = append(cmd.Env[:i], cmd.Env[i+1:]...)
}
}
if realDockerHost != "" {
cmd.Env = append(cmd.Env, "DOCKER_HOST="+realDockerHost)
}
if realDockerTLSVerify {
cmd.Env = append(cmd.Env, "DOCKER_TLS_VERIFY=1")
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
Loading