diff --git a/cli/server/add.go b/cli/server/add.go index 598d91ca..d3e94888 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -144,6 +144,11 @@ of your ~/.ssh/config. Checks for a valid online domain will be skipped: name = internal.ValidateDomain(c) } + // NOTE(d1): reasonable 5 second timeout for connections which can't + // succeed. The connection is attempted twice, so this results in 10 + // seconds. + timeout := client.WithTimeout(5) + if local { created, err := createServerDir(name) if err != nil { @@ -152,7 +157,7 @@ of your ~/.ssh/config. Checks for a valid online domain will be skipped: logrus.Debugf("attempting to create client for %s", name) - if _, err := client.New(name); err != nil { + if _, err := client.New(name, timeout); err != nil { cleanUp(name) logrus.Fatal(err) } @@ -184,9 +189,8 @@ of your ~/.ssh/config. Checks for a valid online domain will be skipped: } logrus.Debugf("attempting to create client for %s", name) - if _, err := client.New(name); err != nil { + if _, err := client.New(name, timeout); err != nil { cleanUp(name) - logrus.Debugf("failed to construct client for %s, saw %s", name, err.Error()) logrus.Fatal(sshPkg.Fatal(name, err)) } diff --git a/pkg/client/client.go b/pkg/client/client.go index f8f5e2d6..e11c4806 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -16,11 +16,26 @@ import ( "github.com/sirupsen/logrus" ) +// Conf is a Docker client configuration. +type Conf struct { + Timeout int +} + +// Opt is a Docker client option. +type Opt func(c *Conf) + +// WithTimeout specifies a timeout for a Docker client. +func WithTimeout(timeout int) Opt { + return func(c *Conf) { + c.Timeout = timeout + } +} + // New initiates a new Docker client. New client connections are validated so // that we ensure connections via SSH to the daemon can succeed. It takes into // account that you may only want the local client and not communicate via SSH. // For this use-case, please pass "default" as the contextName. -func New(serverName string) (*client.Client, error) { +func New(serverName string, opts ...Opt) (*client.Client, error) { var clientOpts []client.Opt if serverName != "default" { @@ -34,7 +49,12 @@ func New(serverName string) (*client.Client, error) { return nil, err } - helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint) + conf := &Conf{} + for _, opt := range opts { + opt(conf) + } + + helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint, conf.Timeout) if err != nil { return nil, err } diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 9a3a9efd..97d627a0 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -19,7 +19,7 @@ func Fatal(hostname string, err error) error { } else if strings.Contains(out, "Permission denied") { return fmt.Errorf("ssh auth: permission denied for %s", hostname) } else if strings.Contains(out, "Network is unreachable") { - return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname) + return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname) } return err diff --git a/pkg/upstream/commandconn/connection.go b/pkg/upstream/commandconn/connection.go index 63886eb9..f48a77af 100644 --- a/pkg/upstream/commandconn/connection.go +++ b/pkg/upstream/commandconn/connection.go @@ -2,6 +2,7 @@ package commandconn import ( "context" + "fmt" "net" "net/url" @@ -14,14 +15,17 @@ import ( ) // GetConnectionHelper returns Docker-specific connection helper for the given URL. -// GetConnectionHelper returns nil without error when no helper is registered for the scheme. -// -// ssh:// URL requires Docker 18.09 or later on the remote host. -func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { - return getConnectionHelper(daemonURL) +func GetConnectionHelper(daemonURL string, timeout int) (*connhelper.ConnectionHelper, error) { + if timeout != 0 { + return getConnectionHelper(daemonURL, []string{fmt.Sprintf("-o ConnectTimeout=%v", timeout)}) + } + + return getConnectionHelper(daemonURL, []string{}) } -func getConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { +// getConnectionHelper generates a connection helper from the underlying Docker +// libraries. +func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) { url, err := url.Parse(daemonURL) if err != nil { return nil, err @@ -35,7 +39,7 @@ func getConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) return &connhelper.ConnectionHelper{ Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { - return New(ctx, "ssh", ctxConnDetails.Args("docker", "system", "dial-stdio")...) + return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...) }, Host: "http://docker.example.com", }, nil @@ -46,8 +50,8 @@ func getConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) } // NewConnectionHelper creates new connection helper for a remote docker daemon. -func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { - helper, err := GetConnectionHelper(daemonURL) +func NewConnectionHelper(daemonURL string, timeout int) (*connhelper.ConnectionHelper, error) { + helper, err := GetConnectionHelper(daemonURL, timeout) if err != nil { return nil, err }