Skip to content

Commit 11fea00

Browse files
authored
Merge pull request #6022 from thaJeztah/connhelper_cleanups_step1
cli/connhelper: cleanups and test improvements
2 parents eeda0af + c771596 commit 11fea00

4 files changed

Lines changed: 124 additions & 34 deletions

File tree

cli/connhelper/commandconn/commandconn.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"syscall"
2929
"time"
3030

31-
"github.com/pkg/errors"
3231
"github.com/sirupsen/logrus"
3332
)
3433

@@ -149,7 +148,7 @@ func (c *commandConn) handleEOF(err error) error {
149148
c.stderrMu.Lock()
150149
stderr := c.stderr.String()
151150
c.stderrMu.Unlock()
152-
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
151+
return fmt.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, err, stderr)
153152
}
154153
}
155154

@@ -159,7 +158,7 @@ func (c *commandConn) handleEOF(err error) error {
159158
c.stderrMu.Lock()
160159
stderr := c.stderr.String()
161160
c.stderrMu.Unlock()
162-
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)
161+
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)
163162
}
164163

165164
func ignorableCloseError(err error) bool {

cli/connhelper/connhelper.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package connhelper
33

44
import (
55
"context"
6+
"fmt"
67
"net"
78
"net/url"
89
"strings"
910

1011
"github.com/docker/cli/cli/connhelper/commandconn"
1112
"github.com/docker/cli/cli/connhelper/ssh"
12-
"github.com/pkg/errors"
1313
)
1414

1515
// ConnectionHelper allows to connect to a remote host with custom stream provider binary.
@@ -43,7 +43,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
4343
if u.Scheme == "ssh" {
4444
sp, err := ssh.ParseURL(daemonURL)
4545
if err != nil {
46-
return nil, errors.Wrap(err, "ssh host connection is not valid")
46+
return nil, fmt.Errorf("ssh host connection is not valid: %w", err)
4747
}
4848
sshFlags = addSSHTimeout(sshFlags)
4949
sshFlags = disablePseudoTerminalAllocation(sshFlags)

cli/connhelper/ssh/ssh.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@
22
package ssh
33

44
import (
5+
"errors"
6+
"fmt"
57
"net/url"
6-
7-
"github.com/pkg/errors"
88
)
99

10-
// ParseURL parses URL
10+
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if
11+
// the URL is using the wrong scheme, contains fragments, query-parameters,
12+
// or contains a password.
1113
func ParseURL(daemonURL string) (*Spec, error) {
1214
u, err := url.Parse(daemonURL)
1315
if err != nil {
14-
return nil, err
16+
var urlErr *url.Error
17+
if errors.As(err, &urlErr) {
18+
err = urlErr.Unwrap()
19+
}
20+
return nil, fmt.Errorf("invalid ssh URL: %w", err)
21+
}
22+
s, err := newSpec(u)
23+
if err != nil {
24+
return nil, fmt.Errorf("invalid ssh URL: %w", err)
25+
}
26+
return s, nil
27+
}
28+
29+
func newSpec(u *url.URL) (*Spec, error) {
30+
if u.Scheme == "" {
31+
return nil, errors.New("no scheme provided")
1532
}
1633
if u.Scheme != "ssh" {
17-
return nil, errors.Errorf("expected scheme ssh, got %q", u.Scheme)
34+
return nil, errors.New("incorrect scheme: " + u.Scheme)
1835
}
1936

2037
var sp Spec
@@ -27,17 +44,18 @@ func ParseURL(daemonURL string) (*Spec, error) {
2744
}
2845
sp.Host = u.Hostname()
2946
if sp.Host == "" {
30-
return nil, errors.Errorf("no host specified")
47+
return nil, errors.New("hostname is empty")
3148
}
3249
sp.Port = u.Port()
3350
sp.Path = u.Path
3451
if u.RawQuery != "" {
35-
return nil, errors.Errorf("extra query after the host: %q", u.RawQuery)
52+
return nil, fmt.Errorf("query parameters are not allowed: %q", u.RawQuery)
3653
}
3754
if u.Fragment != "" {
38-
return nil, errors.Errorf("extra fragment after the host: %q", u.Fragment)
55+
return nil, fmt.Errorf("fragments are not allowed: %q", u.Fragment)
3956
}
40-
return &sp, err
57+
58+
return &sp, nil
4159
}
4260

4361
// Spec of SSH URL

cli/connhelper/ssh/ssh_test.go

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,132 @@ import (
99

1010
func TestParseURL(t *testing.T) {
1111
testCases := []struct {
12+
doc string
1213
url string
14+
remoteCommand []string
1315
expectedArgs []string
1416
expectedError string
17+
expectedSpec Spec
1518
}{
1619
{
17-
url: "ssh://foo",
20+
doc: "bare ssh URL",
21+
url: "ssh://example.com",
1822
expectedArgs: []string{
19-
"--", "foo",
23+
"--", "example.com",
24+
},
25+
expectedSpec: Spec{
26+
Host: "example.com",
2027
},
2128
},
2229
{
23-
url: "ssh://me@foo:10022",
30+
doc: "bare ssh URL and remote command",
31+
url: "ssh://example.com",
32+
remoteCommand: []string{
33+
"docker", "system", "dial-stdio",
34+
},
2435
expectedArgs: []string{
25-
"-l", "me",
26-
"-p", "10022",
27-
"--", "foo",
36+
"--", "example.com",
37+
"docker", "system", "dial-stdio",
38+
},
39+
expectedSpec: Spec{
40+
Host: "example.com",
2841
},
2942
},
3043
{
31-
url: "ssh://me:passw0rd@foo",
32-
expectedError: "plain-text password is not supported",
44+
doc: "ssh URL with path and remote command and flag",
45+
url: "ssh://example.com/var/run/docker.sock",
46+
remoteCommand: []string{
47+
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
48+
},
49+
expectedArgs: []string{
50+
"--", "example.com",
51+
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
52+
},
53+
expectedSpec: Spec{
54+
Host: "example.com",
55+
Path: "/var/run/docker.sock",
56+
},
3357
},
3458
{
35-
url: "ssh://foo/bar",
59+
doc: "ssh URL with username and port",
60+
url: "ssh://me@example.com:10022",
3661
expectedArgs: []string{
37-
"--", "foo",
62+
"-l", "me",
63+
"-p", "10022",
64+
"--", "example.com",
65+
},
66+
expectedSpec: Spec{
67+
User: "me",
68+
Host: "example.com",
69+
Port: "10022",
3870
},
3971
},
4072
{
41-
url: "ssh://foo?bar",
42-
expectedError: `extra query after the host: "bar"`,
73+
doc: "ssh URL with username, port, and path",
74+
url: "ssh://me@example.com:10022/var/run/docker.sock",
75+
expectedArgs: []string{
76+
"-l", "me",
77+
"-p", "10022",
78+
"--", "example.com",
79+
},
80+
expectedSpec: Spec{
81+
User: "me",
82+
Host: "example.com",
83+
Port: "10022",
84+
Path: "/var/run/docker.sock",
85+
},
86+
},
87+
{
88+
doc: "malformed URL",
89+
url: "malformed %%url",
90+
expectedError: `invalid ssh URL: invalid URL escape "%%u"`,
91+
},
92+
{
93+
doc: "URL missing scheme",
94+
url: "no-scheme.example.com",
95+
expectedError: "invalid ssh URL: no scheme provided",
4396
},
4497
{
45-
url: "ssh://foo#bar",
46-
expectedError: `extra fragment after the host: "bar"`,
98+
doc: "invalid URL with password",
99+
url: "ssh://me:passw0rd@example.com",
100+
expectedError: "invalid ssh URL: plain-text password is not supported",
47101
},
48102
{
103+
doc: "invalid URL with query parameter",
104+
url: "ssh://example.com?foo=bar&bar=baz",
105+
expectedError: `invalid ssh URL: query parameters are not allowed: "foo=bar&bar=baz"`,
106+
},
107+
{
108+
doc: "invalid URL with fragment",
109+
url: "ssh://example.com#bar",
110+
expectedError: `invalid ssh URL: fragments are not allowed: "bar"`,
111+
},
112+
{
113+
doc: "invalid URL without hostname",
49114
url: "ssh://",
50-
expectedError: "no host specified",
115+
expectedError: "invalid ssh URL: hostname is empty",
116+
},
117+
{
118+
url: "ssh:///no-hostname",
119+
expectedError: "invalid ssh URL: hostname is empty",
51120
},
52121
{
53-
url: "foo://bar",
54-
expectedError: `expected scheme ssh, got "foo"`,
122+
doc: "invalid URL with unsupported scheme",
123+
url: "https://example.com",
124+
expectedError: `invalid ssh URL: incorrect scheme: https`,
55125
},
56126
}
57127
for _, tc := range testCases {
58-
t.Run(tc.url, func(t *testing.T) {
128+
t.Run(tc.doc, func(t *testing.T) {
59129
sp, err := ParseURL(tc.url)
60130
if tc.expectedError == "" {
61131
assert.NilError(t, err)
62-
assert.Check(t, is.DeepEqual(tc.expectedArgs, sp.Args()))
132+
actualArgs := sp.Args(tc.remoteCommand...)
133+
assert.Check(t, is.DeepEqual(actualArgs, tc.expectedArgs))
134+
assert.Check(t, is.Equal(*sp, tc.expectedSpec))
63135
} else {
64-
assert.ErrorContains(t, err, tc.expectedError)
136+
assert.Check(t, is.Error(err, tc.expectedError))
137+
assert.Check(t, is.Nil(sp))
65138
}
66139
})
67140
}

0 commit comments

Comments
 (0)