Skip to content

Commit 6f61cf0

Browse files
committed
support SSH connection
e.g. docker -H ssh://me@server The `docker` CLI also needs to be installed on the remote host to provide `docker system dial-stdio`, which proxies the daemon socket to stdio. Please refer to docs/reference/commandline/dockerd.md . Signed-off-by: Akihiro Suda <[email protected]>
1 parent 261ff66 commit 6f61cf0

21 files changed

Lines changed: 644 additions & 37 deletions

File tree

cli/command/cli.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/docker/cli/cli/config"
1515
cliconfig "github.com/docker/cli/cli/config"
1616
"github.com/docker/cli/cli/config/configfile"
17+
"github.com/docker/cli/cli/connhelper"
1718
cliflags "github.com/docker/cli/cli/flags"
1819
manifeststore "github.com/docker/cli/cli/manifest/store"
1920
registryclient "github.com/docker/cli/cli/registry/client"
@@ -248,31 +249,54 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerC
248249

249250
// NewAPIClientFromFlags creates a new APIClient from command line flags
250251
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
251-
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
252+
unparsedHost, err := getUnparsedServerHost(opts.Hosts)
252253
if err != nil {
253254
return &client.Client{}, err
254255
}
256+
var clientOpts []func(*client.Client) error
257+
helper, err := connhelper.GetConnectionHelper(unparsedHost)
258+
if err != nil {
259+
return &client.Client{}, err
260+
}
261+
if helper == nil {
262+
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
263+
host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost)
264+
if err != nil {
265+
return &client.Client{}, err
266+
}
267+
clientOpts = append(clientOpts, client.WithHost(host))
268+
} else {
269+
clientOpts = append(clientOpts, func(c *client.Client) error {
270+
httpClient := &http.Client{
271+
// No tls
272+
// No proxy
273+
Transport: &http.Transport{
274+
DialContext: helper.Dialer,
275+
},
276+
}
277+
return client.WithHTTPClient(httpClient)(c)
278+
})
279+
clientOpts = append(clientOpts, client.WithHost(helper.Host))
280+
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
281+
}
255282

256283
customHeaders := configFile.HTTPHeaders
257284
if customHeaders == nil {
258285
customHeaders = map[string]string{}
259286
}
260287
customHeaders["User-Agent"] = UserAgent()
288+
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
261289

262290
verStr := api.DefaultVersion
263291
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
264292
verStr = tmpStr
265293
}
294+
clientOpts = append(clientOpts, client.WithVersion(verStr))
266295

267-
return client.NewClientWithOpts(
268-
withHTTPClient(opts.TLSOptions),
269-
client.WithHTTPHeaders(customHeaders),
270-
client.WithVersion(verStr),
271-
client.WithHost(host),
272-
)
296+
return client.NewClientWithOpts(clientOpts...)
273297
}
274298

275-
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
299+
func getUnparsedServerHost(hosts []string) (string, error) {
276300
var host string
277301
switch len(hosts) {
278302
case 0:
@@ -282,8 +306,7 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
282306
default:
283307
return "", errors.New("Please specify only one -H")
284308
}
285-
286-
return dopts.ParseHost(tlsOptions != nil, host)
309+
return host, nil
287310
}
288311

289312
func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {

cli/command/system/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func NewSystemCommand(dockerCli command.Cli) *cobra.Command {
1919
NewInfoCommand(dockerCli),
2020
newDiskUsageCommand(dockerCli),
2121
newPruneCommand(dockerCli),
22+
newDialStdioCommand(dockerCli),
2223
)
2324

2425
return cmd

cli/command/system/dial_stdio.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package system
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
8+
"github.com/docker/cli/cli"
9+
"github.com/docker/cli/cli/command"
10+
"github.com/pkg/errors"
11+
"github.com/sirupsen/logrus"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
// newDialStdioCommand creates a new cobra.Command for `docker system dial-stdio`
16+
func newDialStdioCommand(dockerCli command.Cli) *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "dial-stdio",
19+
Short: "Proxy the stdio stream to the daemon connection. Should not be invoked manually.",
20+
Args: cli.NoArgs,
21+
Hidden: true,
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
return runDialStdio(dockerCli)
24+
},
25+
}
26+
return cmd
27+
}
28+
29+
func runDialStdio(dockerCli command.Cli) error {
30+
ctx, cancel := context.WithCancel(context.Background())
31+
defer cancel()
32+
dialer := dockerCli.Client().Dialer()
33+
conn, err := dialer(ctx)
34+
if err != nil {
35+
return errors.Wrap(err, "failed to open the raw stream connection")
36+
}
37+
connHalfCloser, ok := conn.(halfCloser)
38+
if !ok {
39+
return errors.New("the raw stream connection does not implement halfCloser")
40+
}
41+
stdin2conn := make(chan error)
42+
conn2stdout := make(chan error)
43+
go func() {
44+
stdin2conn <- copier(connHalfCloser, &halfReadCloserWrapper{os.Stdin}, "stdin to stream")
45+
}()
46+
go func() {
47+
conn2stdout <- copier(&halfWriteCloserWrapper{os.Stdout}, connHalfCloser, "stream to stdout")
48+
}()
49+
select {
50+
case err = <-stdin2conn:
51+
if err != nil {
52+
return err
53+
}
54+
// wait for stdout
55+
err = <-conn2stdout
56+
case err = <-conn2stdout:
57+
// return immediately without waiting for stdin to be closed.
58+
// (stdin is never closed when tty)
59+
}
60+
return err
61+
}
62+
63+
func copier(to halfWriteCloser, from halfReadCloser, debugDescription string) error {
64+
defer func() {
65+
if err := from.CloseRead(); err != nil {
66+
logrus.Errorf("error while CloseRead (%s): %v", debugDescription, err)
67+
}
68+
if err := to.CloseWrite(); err != nil {
69+
logrus.Errorf("error while CloseWrite (%s): %v", debugDescription, err)
70+
}
71+
}()
72+
if _, err := io.Copy(to, from); err != nil {
73+
return errors.Wrapf(err, "error while Copy (%s)", debugDescription)
74+
}
75+
return nil
76+
}
77+
78+
type halfReadCloser interface {
79+
io.Reader
80+
CloseRead() error
81+
}
82+
83+
type halfWriteCloser interface {
84+
io.Writer
85+
CloseWrite() error
86+
}
87+
88+
type halfCloser interface {
89+
halfReadCloser
90+
halfWriteCloser
91+
}
92+
93+
type halfReadCloserWrapper struct {
94+
io.ReadCloser
95+
}
96+
97+
func (x *halfReadCloserWrapper) CloseRead() error {
98+
return x.Close()
99+
}
100+
101+
type halfWriteCloserWrapper struct {
102+
io.WriteCloser
103+
}
104+
105+
func (x *halfWriteCloserWrapper) CloseWrite() error {
106+
return x.Close()
107+
}

0 commit comments

Comments
 (0)