Merge pull request #3831 from dgageot/support-docker-over-ssh

Support remote docker over ssh
This commit is contained in:
Avi Deitcher 2022-10-07 14:43:35 +03:00 committed by GitHub
commit 07adfa2bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 166 additions and 6 deletions

View File

@ -5,23 +5,49 @@ import (
"errors"
"io"
"os"
"sync"
"github.com/containerd/containerd/reference"
"github.com/docker/cli/cli/connhelper"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
)
var (
clientOnce sync.Once
memoizedClient *client.Client
errClient error
)
// Client get a docker client.
func Client() (*client.Client, error) {
// for maximum compatibility as we use nothing new
// 1.30 corresponds to Docker 17.06, supported until 2020.
err := os.Setenv("DOCKER_API_VERSION", "1.30")
if err != nil {
return nil, err
clientOnce.Do(func() {
memoizedClient, errClient = createClient()
})
return memoizedClient, errClient
}
func createClient() (*client.Client, error) {
options := []client.Opt{
client.WithAPIVersionNegotiation(),
client.WithTLSClientConfigFromEnv(),
client.WithHostFromEnv(),
}
return client.NewEnvClient()
// Support connection over ssh.
if host := os.Getenv(client.EnvOverrideHost); host != "" {
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
return nil, err
}
if helper != nil {
options = append(options, client.WithDialContext(helper.Dialer))
}
}
return client.NewClientWithOpts(options...)
}
// HasImage check if the provided ref is available in the docker cache.

View File

@ -0,0 +1,68 @@
// Package connhelper provides helpers for connecting to a remote daemon host with custom logic.
package connhelper
import (
"context"
"net"
"net/url"
"github.com/docker/cli/cli/connhelper/commandconn"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/pkg/errors"
)
// 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.
//
// ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host.
func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) {
return getConnectionHelper(daemonURL, nil)
}
// GetConnectionHelperWithSSHOpts returns Docker-specific connection helper for
// the given URL, and accepts additional options for ssh connections. It returns
// nil without error when no helper is registered for the scheme.
//
// Requires Docker 18.09 or later on the remote host.
func GetConnectionHelperWithSSHOpts(daemonURL string, sshFlags []string) (*ConnectionHelper, error) {
return getConnectionHelper(daemonURL, sshFlags)
}
func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper, error) {
u, err := url.Parse(daemonURL)
if err != nil {
return nil, err
}
switch scheme := u.Scheme; scheme {
case "ssh":
sp, err := ssh.ParseURL(daemonURL)
if err != nil {
return nil, errors.Wrap(err, "ssh host connection is not valid")
}
return &ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args("docker", "system", "dial-stdio")...)...)
},
Host: "http://docker.example.com",
}, nil
}
// Future version may support plugins via ~/.docker/config.json. e.g. "dind"
// See docker/cli#889 for the previous discussion.
return nil, err
}
// GetCommandConnectionHelper returns Docker-specific connection helper constructed from an arbitrary command.
func GetCommandConnectionHelper(cmd string, flags ...string) (*ConnectionHelper, error) {
return &ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return commandconn.New(ctx, cmd, flags...)
},
Host: "http://docker.example.com",
}, nil
}

View File

@ -0,0 +1,64 @@
// Package ssh provides the connection helper for ssh:// URL.
package ssh
import (
"net/url"
"github.com/pkg/errors"
)
// ParseURL parses URL
func ParseURL(daemonURL string) (*Spec, error) {
u, err := url.Parse(daemonURL)
if err != nil {
return nil, err
}
if u.Scheme != "ssh" {
return nil, errors.Errorf("expected scheme ssh, got %q", u.Scheme)
}
var sp Spec
if u.User != nil {
sp.User = u.User.Username()
if _, ok := u.User.Password(); ok {
return nil, errors.New("plain-text password is not supported")
}
}
sp.Host = u.Hostname()
if sp.Host == "" {
return nil, errors.Errorf("no host specified")
}
sp.Port = u.Port()
if u.Path != "" {
return nil, errors.Errorf("extra path after the host: %q", u.Path)
}
if u.RawQuery != "" {
return nil, errors.Errorf("extra query after the host: %q", u.RawQuery)
}
if u.Fragment != "" {
return nil, errors.Errorf("extra fragment after the host: %q", u.Fragment)
}
return &sp, err
}
// Spec of SSH URL
type Spec struct {
User string
Host string
Port string
}
// Args returns args except "ssh" itself combined with optional additional command args
func (sp *Spec) Args(add ...string) []string {
var args []string
if sp.User != "" {
args = append(args, "-l", sp.User)
}
if sp.Port != "" {
args = append(args, "-p", sp.Port)
}
args = append(args, "--", sp.Host)
args = append(args, add...)
return args
}

View File

@ -160,7 +160,9 @@ github.com/docker/cli/cli/config
github.com/docker/cli/cli/config/configfile
github.com/docker/cli/cli/config/credentials
github.com/docker/cli/cli/config/types
github.com/docker/cli/cli/connhelper
github.com/docker/cli/cli/connhelper/commandconn
github.com/docker/cli/cli/connhelper/ssh
# github.com/docker/distribution v2.8.1+incompatible
github.com/docker/distribution
github.com/docker/distribution/digestset