88 "fmt"
99 "io"
1010 "io/fs"
11+ "net/http"
1112 "os"
1213 "path/filepath"
1314 "runtime"
@@ -48,6 +49,43 @@ type sshConfigOptions struct {
4849 sshOptions []string
4950}
5051
52+ // addOptions expects options in the form of "option=value" or "option value".
53+ // It will override any existing option with the same key to prevent duplicates.
54+ // Invalid options will return an error.
55+ func (o * sshConfigOptions ) addOptions (options ... string ) error {
56+ for _ , option := range options {
57+ err := o .addOption (option )
58+ if err != nil {
59+ return err
60+ }
61+ }
62+ return nil
63+ }
64+
65+ func (o * sshConfigOptions ) addOption (option string ) error {
66+ key , _ , err := codersdk .ParseSSHConfigOption (option )
67+ if err != nil {
68+ return err
69+ }
70+ for i , existing := range o .sshOptions {
71+ // Override existing option if they share the same key.
72+ // This is case-insensitive. Parsing each time might be a little slow,
73+ // but it is ok.
74+ existingKey , _ , err := codersdk .ParseSSHConfigOption (existing )
75+ if err != nil {
76+ // Don't mess with original values if there is an error.
77+ // This could have come from the user's manual edits.
78+ continue
79+ }
80+ if strings .EqualFold (existingKey , key ) {
81+ o .sshOptions [i ] = option
82+ return nil
83+ }
84+ }
85+ o .sshOptions = append (o .sshOptions , option )
86+ return nil
87+ }
88+
5189func (o sshConfigOptions ) equal (other sshConfigOptions ) bool {
5290 // Compare without side-effects or regard to order.
5391 opt1 := slices .Clone (o .sshOptions )
@@ -139,6 +177,7 @@ func configSSH() *cobra.Command {
139177 usePreviousOpts bool
140178 dryRun bool
141179 skipProxyCommand bool
180+ userHostPrefix string
142181 )
143182 cmd := & cobra.Command {
144183 Annotations : workspaceCommand ,
@@ -156,12 +195,13 @@ func configSSH() *cobra.Command {
156195 ),
157196 Args : cobra .ExactArgs (0 ),
158197 RunE : func (cmd * cobra.Command , _ []string ) error {
198+ ctx := cmd .Context ()
159199 client , err := CreateClient (cmd )
160200 if err != nil {
161201 return err
162202 }
163203
164- recvWorkspaceConfigs := sshPrepareWorkspaceConfigs (cmd . Context () , client )
204+ recvWorkspaceConfigs := sshPrepareWorkspaceConfigs (ctx , client )
165205
166206 out := cmd .OutOrStdout ()
167207 if dryRun {
@@ -220,6 +260,13 @@ func configSSH() *cobra.Command {
220260 if usePreviousOpts && lastConfig != nil {
221261 sshConfigOpts = * lastConfig
222262 } else if lastConfig != nil && ! sshConfigOpts .equal (* lastConfig ) {
263+ for _ , v := range sshConfigOpts .sshOptions {
264+ // If the user passes an invalid option, we should catch
265+ // this early.
266+ if _ , _ , err := codersdk .ParseSSHConfigOption (v ); err != nil {
267+ return xerrors .Errorf ("invalid option from flag: %w" , err )
268+ }
269+ }
223270 newOpts := sshConfigOpts .asList ()
224271 newOptsMsg := "\n \n New options: none"
225272 if len (newOpts ) > 0 {
@@ -269,42 +316,85 @@ func configSSH() *cobra.Command {
269316 if err != nil {
270317 return xerrors .Errorf ("fetch workspace configs failed: %w" , err )
271318 }
319+
320+ coderdConfig , err := client .SSHConfiguration (ctx )
321+ if err != nil {
322+ // If the error is 404, this deployment does not support
323+ // this endpoint yet. Do not error, just assume defaults.
324+ // TODO: Remove this in 2 months (May 31, 2023). Just return the error
325+ // and remove this 404 check.
326+ var sdkErr * codersdk.Error
327+ if ! (xerrors .As (err , & sdkErr ) && sdkErr .StatusCode () == http .StatusNotFound ) {
328+ return xerrors .Errorf ("fetch coderd config failed: %w" , err )
329+ }
330+ coderdConfig .HostnamePrefix = "coder."
331+ }
332+
333+ if userHostPrefix != "" {
334+ // Override with user flag.
335+ coderdConfig .HostnamePrefix = userHostPrefix
336+ }
337+
272338 // Ensure stable sorting of output.
273339 slices .SortFunc (workspaceConfigs , func (a , b sshWorkspaceConfig ) bool {
274340 return a .Name < b .Name
275341 })
276342 for _ , wc := range workspaceConfigs {
277343 sort .Strings (wc .Hosts )
278344 // Write agent configuration.
279- for _ , hostname := range wc .Hosts {
280- configOptions := []string {
281- "Host coder." + hostname ,
282- }
283- for _ , option := range sshConfigOpts .sshOptions {
284- configOptions = append (configOptions , "\t " + option )
285- }
286- configOptions = append (configOptions ,
287- "\t HostName coder." + hostname ,
288- "\t ConnectTimeout=0" ,
289- "\t StrictHostKeyChecking=no" ,
345+ for _ , workspaceHostname := range wc .Hosts {
346+ sshHostname := fmt .Sprintf ("%s%s" , coderdConfig .HostnamePrefix , workspaceHostname )
347+ defaultOptions := []string {
348+ "HostName " + sshHostname ,
349+ "ConnectTimeout=0" ,
350+ "StrictHostKeyChecking=no" ,
290351 // Without this, the "REMOTE HOST IDENTITY CHANGED"
291352 // message will appear.
292- "\t UserKnownHostsFile =/dev/null" ,
353+ "UserKnownHostsFile =/dev/null" ,
293354 // This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
294355 // message from appearing on every SSH. This happens because we ignore the known hosts.
295- "\t LogLevel ERROR" ,
296- )
356+ "LogLevel ERROR" ,
357+ }
358+
297359 if ! skipProxyCommand {
298- configOptions = append (
299- configOptions ,
300- fmt .Sprintf (
301- "\t ProxyCommand %s --global-config %s ssh --stdio %s" ,
302- escapedCoderBinary , escapedGlobalConfig , hostname ,
303- ),
304- )
360+ defaultOptions = append (defaultOptions , fmt .Sprintf (
361+ "ProxyCommand %s --global-config %s ssh --stdio %s" ,
362+ escapedCoderBinary , escapedGlobalConfig , workspaceHostname ,
363+ ))
364+ }
365+
366+ var configOptions sshConfigOptions
367+ // Add standard options.
368+ err := configOptions .addOptions (defaultOptions ... )
369+ if err != nil {
370+ return err
371+ }
372+
373+ // Override with deployment options
374+ for k , v := range coderdConfig .SSHConfigOptions {
375+ opt := fmt .Sprintf ("%s %s" , k , v )
376+ err := configOptions .addOptions (opt )
377+ if err != nil {
378+ return xerrors .Errorf ("add coderd config option %q: %w" , opt , err )
379+ }
380+ }
381+ // Override with flag options
382+ for _ , opt := range sshConfigOpts .sshOptions {
383+ err := configOptions .addOptions (opt )
384+ if err != nil {
385+ return xerrors .Errorf ("add flag config option %q: %w" , opt , err )
386+ }
387+ }
388+
389+ hostBlock := []string {
390+ "Host " + sshHostname ,
391+ }
392+ // Prefix with '\t'
393+ for _ , v := range configOptions .sshOptions {
394+ hostBlock = append (hostBlock , "\t " + v )
305395 }
306396
307- _ , _ = buf .WriteString (strings .Join (configOptions , "\n " ))
397+ _ , _ = buf .WriteString (strings .Join (hostBlock , "\n " ))
308398 _ = buf .WriteByte ('\n' )
309399 }
310400 }
@@ -363,7 +453,7 @@ func configSSH() *cobra.Command {
363453
364454 if len (workspaceConfigs ) > 0 {
365455 _ , _ = fmt .Fprintln (out , "You should now be able to ssh into your workspace." )
366- _ , _ = fmt .Fprintf (out , "For example, try running:\n \n \t $ ssh coder.%s \n " , workspaceConfigs [0 ].Name )
456+ _ , _ = fmt .Fprintf (out , "For example, try running:\n \n \t $ ssh %s%s \n " , coderdConfig . HostnamePrefix , workspaceConfigs [0 ].Name )
367457 } else {
368458 _ , _ = fmt .Fprint (out , "You don't have any workspaces yet, try creating one with:\n \n \t $ coder create <workspace>\n " )
369459 }
@@ -376,6 +466,7 @@ func configSSH() *cobra.Command {
376466 cmd .Flags ().BoolVarP (& skipProxyCommand , "skip-proxy-command" , "" , false , "Specifies whether the ProxyCommand option should be skipped. Useful for testing." )
377467 _ = cmd .Flags ().MarkHidden ("skip-proxy-command" )
378468 cliflag .BoolVarP (cmd .Flags (), & usePreviousOpts , "use-previous-options" , "" , "CODER_SSH_USE_PREVIOUS_OPTIONS" , false , "Specifies whether or not to keep options from previous run of config-ssh." )
469+ cmd .Flags ().StringVarP (& userHostPrefix , "ssh-host-prefix" , "" , "" , "Override the default host prefix." )
379470 cliui .AllowSkipPrompt (cmd )
380471
381472 return cmd
0 commit comments