Merge pull request #6022 from thaJeztah/connhelper_cleanups_step1
cli/connhelper: cleanups and test improvements
This commit is contained in:
commit
11fea00142
@ -28,7 +28,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,7 +148,7 @@ func (c *commandConn) handleEOF(err error) error {
|
|||||||
c.stderrMu.Lock()
|
c.stderrMu.Lock()
|
||||||
stderr := c.stderr.String()
|
stderr := c.stderr.String()
|
||||||
c.stderrMu.Unlock()
|
c.stderrMu.Unlock()
|
||||||
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
|
return fmt.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +158,7 @@ func (c *commandConn) handleEOF(err error) error {
|
|||||||
c.stderrMu.Lock()
|
c.stderrMu.Lock()
|
||||||
stderr := c.stderr.String()
|
stderr := c.stderr.String()
|
||||||
c.stderrMu.Unlock()
|
c.stderrMu.Unlock()
|
||||||
return errors.Errorf("command %v has exited with %v, make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr)
|
return fmt.Errorf("command %v has exited with %v, make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ignorableCloseError(err error) bool {
|
func ignorableCloseError(err error) bool {
|
||||||
|
@ -3,13 +3,13 @@ package connhelper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper/commandconn"
|
"github.com/docker/cli/cli/connhelper/commandconn"
|
||||||
"github.com/docker/cli/cli/connhelper/ssh"
|
"github.com/docker/cli/cli/connhelper/ssh"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
|
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
|
||||||
@ -43,7 +43,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
|
|||||||
if u.Scheme == "ssh" {
|
if u.Scheme == "ssh" {
|
||||||
sp, err := ssh.ParseURL(daemonURL)
|
sp, err := ssh.ParseURL(daemonURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
return nil, fmt.Errorf("ssh host connection is not valid: %w", err)
|
||||||
}
|
}
|
||||||
sshFlags = addSSHTimeout(sshFlags)
|
sshFlags = addSSHTimeout(sshFlags)
|
||||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||||
|
@ -2,19 +2,36 @@
|
|||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseURL parses URL
|
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if
|
||||||
|
// the URL is using the wrong scheme, contains fragments, query-parameters,
|
||||||
|
// or contains a password.
|
||||||
func ParseURL(daemonURL string) (*Spec, error) {
|
func ParseURL(daemonURL string) (*Spec, error) {
|
||||||
u, err := url.Parse(daemonURL)
|
u, err := url.Parse(daemonURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
var urlErr *url.Error
|
||||||
|
if errors.As(err, &urlErr) {
|
||||||
|
err = urlErr.Unwrap()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid ssh URL: %w", err)
|
||||||
|
}
|
||||||
|
s, err := newSpec(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ssh URL: %w", err)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSpec(u *url.URL) (*Spec, error) {
|
||||||
|
if u.Scheme == "" {
|
||||||
|
return nil, errors.New("no scheme provided")
|
||||||
}
|
}
|
||||||
if u.Scheme != "ssh" {
|
if u.Scheme != "ssh" {
|
||||||
return nil, errors.Errorf("expected scheme ssh, got %q", u.Scheme)
|
return nil, errors.New("incorrect scheme: " + u.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sp Spec
|
var sp Spec
|
||||||
@ -27,17 +44,18 @@ func ParseURL(daemonURL string) (*Spec, error) {
|
|||||||
}
|
}
|
||||||
sp.Host = u.Hostname()
|
sp.Host = u.Hostname()
|
||||||
if sp.Host == "" {
|
if sp.Host == "" {
|
||||||
return nil, errors.Errorf("no host specified")
|
return nil, errors.New("hostname is empty")
|
||||||
}
|
}
|
||||||
sp.Port = u.Port()
|
sp.Port = u.Port()
|
||||||
sp.Path = u.Path
|
sp.Path = u.Path
|
||||||
if u.RawQuery != "" {
|
if u.RawQuery != "" {
|
||||||
return nil, errors.Errorf("extra query after the host: %q", u.RawQuery)
|
return nil, fmt.Errorf("query parameters are not allowed: %q", u.RawQuery)
|
||||||
}
|
}
|
||||||
if u.Fragment != "" {
|
if u.Fragment != "" {
|
||||||
return nil, errors.Errorf("extra fragment after the host: %q", u.Fragment)
|
return nil, fmt.Errorf("fragments are not allowed: %q", u.Fragment)
|
||||||
}
|
}
|
||||||
return &sp, err
|
|
||||||
|
return &sp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spec of SSH URL
|
// Spec of SSH URL
|
||||||
|
@ -9,59 +9,132 @@ import (
|
|||||||
|
|
||||||
func TestParseURL(t *testing.T) {
|
func TestParseURL(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
doc string
|
||||||
url string
|
url string
|
||||||
|
remoteCommand []string
|
||||||
expectedArgs []string
|
expectedArgs []string
|
||||||
expectedError string
|
expectedError string
|
||||||
|
expectedSpec Spec
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
url: "ssh://foo",
|
doc: "bare ssh URL",
|
||||||
|
url: "ssh://example.com",
|
||||||
expectedArgs: []string{
|
expectedArgs: []string{
|
||||||
"--", "foo",
|
"--", "example.com",
|
||||||
|
},
|
||||||
|
expectedSpec: Spec{
|
||||||
|
Host: "example.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "ssh://me@foo:10022",
|
doc: "bare ssh URL and remote command",
|
||||||
|
url: "ssh://example.com",
|
||||||
|
remoteCommand: []string{
|
||||||
|
"docker", "system", "dial-stdio",
|
||||||
|
},
|
||||||
|
expectedArgs: []string{
|
||||||
|
"--", "example.com",
|
||||||
|
"docker", "system", "dial-stdio",
|
||||||
|
},
|
||||||
|
expectedSpec: Spec{
|
||||||
|
Host: "example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "ssh URL with path and remote command and flag",
|
||||||
|
url: "ssh://example.com/var/run/docker.sock",
|
||||||
|
remoteCommand: []string{
|
||||||
|
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
|
||||||
|
},
|
||||||
|
expectedArgs: []string{
|
||||||
|
"--", "example.com",
|
||||||
|
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
|
||||||
|
},
|
||||||
|
expectedSpec: Spec{
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/var/run/docker.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "ssh URL with username and port",
|
||||||
|
url: "ssh://me@example.com:10022",
|
||||||
expectedArgs: []string{
|
expectedArgs: []string{
|
||||||
"-l", "me",
|
"-l", "me",
|
||||||
"-p", "10022",
|
"-p", "10022",
|
||||||
"--", "foo",
|
"--", "example.com",
|
||||||
|
},
|
||||||
|
expectedSpec: Spec{
|
||||||
|
User: "me",
|
||||||
|
Host: "example.com",
|
||||||
|
Port: "10022",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "ssh://me:passw0rd@foo",
|
doc: "ssh URL with username, port, and path",
|
||||||
expectedError: "plain-text password is not supported",
|
url: "ssh://me@example.com:10022/var/run/docker.sock",
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "ssh://foo/bar",
|
|
||||||
expectedArgs: []string{
|
expectedArgs: []string{
|
||||||
"--", "foo",
|
"-l", "me",
|
||||||
|
"-p", "10022",
|
||||||
|
"--", "example.com",
|
||||||
|
},
|
||||||
|
expectedSpec: Spec{
|
||||||
|
User: "me",
|
||||||
|
Host: "example.com",
|
||||||
|
Port: "10022",
|
||||||
|
Path: "/var/run/docker.sock",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "ssh://foo?bar",
|
doc: "malformed URL",
|
||||||
expectedError: `extra query after the host: "bar"`,
|
url: "malformed %%url",
|
||||||
|
expectedError: `invalid ssh URL: invalid URL escape "%%u"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "ssh://foo#bar",
|
doc: "URL missing scheme",
|
||||||
expectedError: `extra fragment after the host: "bar"`,
|
url: "no-scheme.example.com",
|
||||||
|
expectedError: "invalid ssh URL: no scheme provided",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
doc: "invalid URL with password",
|
||||||
|
url: "ssh://me:passw0rd@example.com",
|
||||||
|
expectedError: "invalid ssh URL: plain-text password is not supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "invalid URL with query parameter",
|
||||||
|
url: "ssh://example.com?foo=bar&bar=baz",
|
||||||
|
expectedError: `invalid ssh URL: query parameters are not allowed: "foo=bar&bar=baz"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "invalid URL with fragment",
|
||||||
|
url: "ssh://example.com#bar",
|
||||||
|
expectedError: `invalid ssh URL: fragments are not allowed: "bar"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "invalid URL without hostname",
|
||||||
url: "ssh://",
|
url: "ssh://",
|
||||||
expectedError: "no host specified",
|
expectedError: "invalid ssh URL: hostname is empty",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "foo://bar",
|
url: "ssh:///no-hostname",
|
||||||
expectedError: `expected scheme ssh, got "foo"`,
|
expectedError: "invalid ssh URL: hostname is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "invalid URL with unsupported scheme",
|
||||||
|
url: "https://example.com",
|
||||||
|
expectedError: `invalid ssh URL: incorrect scheme: https`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.url, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
sp, err := ParseURL(tc.url)
|
sp, err := ParseURL(tc.url)
|
||||||
if tc.expectedError == "" {
|
if tc.expectedError == "" {
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Check(t, is.DeepEqual(tc.expectedArgs, sp.Args()))
|
actualArgs := sp.Args(tc.remoteCommand...)
|
||||||
|
assert.Check(t, is.DeepEqual(actualArgs, tc.expectedArgs))
|
||||||
|
assert.Check(t, is.Equal(*sp, tc.expectedSpec))
|
||||||
} else {
|
} else {
|
||||||
assert.ErrorContains(t, err, tc.expectedError)
|
assert.Check(t, is.Error(err, tc.expectedError))
|
||||||
|
assert.Check(t, is.Nil(sp))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user