Skip to content

Commit 741e4a6

Browse files
authored
Allow SSH options to be given in login URL (#130)
1 parent 7b84aba commit 741e4a6

File tree

6 files changed

+94
-25
lines changed

6 files changed

+94
-25
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"mode": "auto",
5656
"program": "cli/cmd/tyger",
5757
"cwd": "${workspaceFolder}",
58-
"args": ["buffer", "write", "http+unix:///tmp/xyz?relay=true"],
58+
"args": ["login", "status", "--format", "json"],
5959
"env": {
6060
"TYGER_CACHE_FILE": "/home/vscode/.cache/tyger/.tyger-docker"
6161
},

cli/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ require (
4646
github.com/stretchr/testify v1.9.0
4747
go.opentelemetry.io/otel v1.28.0
4848
golang.org/x/sync v0.7.0
49+
golang.org/x/term v0.21.0
4950
helm.sh/helm/v3 v3.14.3
5051
k8s.io/api v0.29.3
5152
k8s.io/apimachinery v0.29.3
@@ -177,7 +178,6 @@ require (
177178
golang.org/x/mod v0.17.0 // indirect
178179
golang.org/x/net v0.26.0 // indirect
179180
golang.org/x/oauth2 v0.20.0 // indirect
180-
golang.org/x/term v0.21.0 // indirect
181181
golang.org/x/text v0.16.0 // indirect
182182
golang.org/x/time v0.5.0 // indirect
183183
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect

cli/internal/client/sshurl.go

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ package client
66
import (
77
"fmt"
88
"net/url"
9+
"os"
910
"runtime"
11+
"strings"
1012

1113
"github.com/google/uuid"
1214
"github.com/pkg/errors"
15+
"golang.org/x/term"
1316
)
1417

1518
// Parses and formats ssh:// URLs
@@ -20,6 +23,7 @@ type SshParams struct {
2023
User string
2124
SocketPath string
2225
CliPath string
26+
Options map[string]string
2327
}
2428

2529
func ParseSshUrl(u *url.URL) (*SshParams, error) {
@@ -48,8 +52,16 @@ func ParseSshUrl(u *url.URL) (*SshParams, error) {
4852
queryParams.Del("cliPath")
4953
}
5054

51-
if len(queryParams) > 0 {
52-
return nil, errors.Errorf("unexpected query parameters: %v. Only 'cliPath' is supported", queryParams)
55+
for k, v := range queryParams {
56+
if strings.HasPrefix(k, "option[") && strings.HasSuffix(k, "]") {
57+
name := k[7 : len(k)-1]
58+
if sp.Options == nil {
59+
sp.Options = make(map[string]string)
60+
}
61+
sp.Options[name] = v[0]
62+
} else {
63+
return nil, errors.Errorf("unexpected query parameter: %q. Only 'cliPath' and 'option[<SSH_OPTION>]' are suported", k)
64+
}
5365
}
5466
}
5567

@@ -77,30 +89,58 @@ func (sp *SshParams) URL() *url.URL {
7789
if sp.Port != "" {
7890
u.Host += ":" + sp.Port
7991
}
92+
q := u.Query()
8093
if sp.CliPath != "" {
81-
q := u.Query()
8294
q.Set("cliPath", sp.CliPath)
95+
}
96+
for k, v := range sp.Options {
97+
q.Set(fmt.Sprintf("option[%s]", k), v)
98+
}
99+
100+
if len(q) > 0 {
83101
u.RawQuery = q.Encode()
84102
}
85103

86104
return &u
87105
}
88106

89107
func (sp *SshParams) FormatCmdLine(add ...string) []string {
90-
return sp.formatCmdLine(nil, add...)
108+
sshOptions := map[string]string{
109+
"StrictHostKeyChecking": "yes",
110+
}
111+
return sp.formatCmdLine(sshOptions, nil, add...)
91112
}
92113

93-
func (sp *SshParams) formatCmdLine(sshArgs []string, cmdArgs ...string) []string {
114+
func (sp *SshParams) formatCmdLine(sshOptions map[string]string, otherSshArgs []string, cmdArgs ...string) []string {
94115
args := []string{sp.Host}
95116

117+
var combinedSshOptions map[string]string
118+
if sp.Options == nil {
119+
combinedSshOptions = sshOptions
120+
} else if sshOptions == nil {
121+
combinedSshOptions = sp.Options
122+
} else {
123+
combinedSshOptions = make(map[string]string)
124+
for k, v := range sshOptions {
125+
combinedSshOptions[k] = v
126+
}
127+
for k, v := range sp.Options {
128+
combinedSshOptions[k] = v
129+
}
130+
}
131+
132+
for k, v := range combinedSshOptions {
133+
args = append(args, "-o", fmt.Sprintf("%s=%s", k, v))
134+
}
135+
96136
if sp.User != "" {
97137
args = append(args, "-l", sp.User)
98138
}
99139
if sp.Port != "" {
100140
args = append(args, "-p", sp.Port)
101141
}
102142

103-
args = append(args, sshArgs...)
143+
args = append(args, otherSshArgs...)
104144

105145
args = append(args, "--")
106146

@@ -117,31 +157,40 @@ func (sp *SshParams) formatCmdLine(sshArgs []string, cmdArgs ...string) []string
117157
}
118158

119159
func (sp *SshParams) FormatLoginArgs(add ...string) []string {
160+
var sshOptions map[string]string
161+
if !term.IsTerminal(int(os.Stdin.Fd())) {
162+
// avoid interactive prompt
163+
sshOptions = map[string]string{
164+
"StrictHostKeyChecking": "yes",
165+
}
166+
}
167+
120168
// Disable stdin and disable pseudo-terminal allocation.
121169
// On Windows, we can get a hang when the remote process exits quickly because
122170
// the SSH process is waiting for Enter to be pressed.
123171

124-
sshArgs := []string{"-nT"}
172+
otherSshArgs := []string{"-nT"}
125173
args := []string{"login"}
126174

127175
if sp.SocketPath != "" {
128176
args = append(args, "--socket-path", sp.SocketPath)
129177
}
130178

131179
args = append(args, add...)
132-
return sp.formatCmdLine(sshArgs, args...)
180+
return sp.formatCmdLine(sshOptions, otherSshArgs, args...)
133181
}
134182

135183
func (sp *SshParams) FormatDataPlaneCmdLine(add ...string) []string {
136-
var sshArgs []string
184+
sshOptions := map[string]string{
185+
"StrictHostKeyChecking": "yes",
186+
}
187+
137188
if runtime.GOOS != "windows" {
138189
// create a dedicated control socket for this process
139-
sshArgs = []string{
140-
"-o", "ControlMaster=auto",
141-
"-o", fmt.Sprintf("ControlPath=/tmp/%s", uuid.New().String()),
142-
"-o", "ControlPersist=2m",
143-
}
190+
sshOptions["ControlMaster"] = "auto"
191+
sshOptions["ControlPath"] = fmt.Sprintf("/tmp/%s", uuid.New().String())
192+
sshOptions["ControlPersist"] = "2m"
144193
}
145194

146-
return sp.formatCmdLine(sshArgs, add...)
195+
return sp.formatCmdLine(sshOptions, nil, add...)
147196
}

cli/internal/cmd/run.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,9 +868,24 @@ func pullImages(ctx context.Context, newRun model.Run) error {
868868
return fmt.Errorf("failed to parse SSH URL: %w", err)
869869
}
870870

871+
// Capture SSH options
872+
defaultArgs := sshParams.FormatCmdLine()
873+
optionArgs := []string{
874+
"-o", "ConnectTimeout=none", // The default of 30s that docker adds can cause a _hang_ of 30s: https://github.com/PowerShell/Win32-OpenSSH/issues/1352
875+
}
876+
for i := 0; i < len(defaultArgs); i++ {
877+
if defaultArgs[i] == "-o" {
878+
optionArgs = append(optionArgs, defaultArgs[i], defaultArgs[i+1])
879+
i++
880+
}
881+
}
882+
883+
// clear fields that result in a URI that docker won't underatand
871884
sshParams.CliPath = ""
872885
sshParams.SocketPath = ""
873-
connhelper, err := connhelper.GetConnectionHelper(sshParams.URL().String())
886+
sshParams.Options = nil
887+
888+
connhelper, err := connhelper.GetConnectionHelperWithSSHOpts(sshParams.URL().String(), optionArgs)
874889
if err != nil {
875890
return fmt.Errorf("failed to get connection helper: %w", err)
876891
}

docs/introduction/installation/docker-installation.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,19 @@ not a password.
149149
The format of the SSH URL is:
150150

151151
```
152-
ssh://[user@]host[:port][?cliPath=/path/to/tyger]
152+
ssh://[user@]host[:port][?key1=value1&key2=value2]
153153
```
154154

155155
All values in `[]` are optional. The user and port default values will come from
156-
your SSH config file (~/.ssh/config). The `cliPath` query parameter only needs
157-
to be specified if `tyger` is not installed in a directory in the SSH host's
158-
$PATH variable.
156+
your SSH config file (~/.ssh/config). Additional parameters can be passed in
157+
as query parameters (after the `?`). These are:
158+
159+
- `cliPath`, to speciy that path to the `tyger` CLI on the host. This is only
160+
necessary if the localtion is not part of the `PATH` variable.
161+
- `option[sshConfigKey]`, to specify additional SSH
162+
[configuration options](https://www.man7.org/linux/man-pages/man5/ssh_config.5.html).
163+
For example, `ssh://myhost?option[StrictHostChecking]=no` results in a SSH command
164+
that looks like `ssh myhost -o StrictHostChecking=no`
159165

160166
For the best user experience with SSH, configure ~/.ssh/config as follows to
161167
allow reusing a SSH connection for multiple invocations of the `tyger` CLI:

scripts/run-ssh-tests.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ Host $ssh_host
110110
HostName $ssh_connection_host
111111
Port $ssh_connection_port
112112
User $ssh_user
113-
StrictHostKeyChecking no
114113
ControlMaster auto
115114
ControlPath ~/.ssh/control-%C
116115
ControlPersist yes
@@ -127,7 +126,7 @@ ssh-keygen -f "${HOME}/.ssh/known_hosts" -R "$ssh_connection_host"
127126

128127
max_attempts=30
129128
attempts=0
130-
until ssh $ssh_host true &>/dev/null || [ $attempts -eq $max_attempts ]; do
129+
until ssh $ssh_host -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null true &>/dev/null || [ $attempts -eq $max_attempts ]; do
131130
echo "Waiting for SSH server to be ready..."
132131
sleep 1
133132
attempts="$((attempts + 1))"
@@ -143,7 +142,7 @@ echo "SSH server is ready"
143142
TYGER_CACHE_FILE=$(mktemp)
144143
export TYGER_CACHE_FILE
145144

146-
tyger login ssh://$ssh_host
145+
tyger login "ssh://$ssh_host?option[StrictHostKeyChecking]=no"
147146
tyger login status
148147

149148
if [[ -z ${start_only:-} ]]; then

0 commit comments

Comments
 (0)