@@ -115,6 +115,7 @@ fun ensureCLI(
115115data class Features (
116116 val disableAutostart : Boolean = false ,
117117 val reportWorkspaceUsage : Boolean = false ,
118+ val wildcardSSH : Boolean = false ,
118119)
119120
120121/* *
@@ -285,37 +286,57 @@ class CoderCLIManager(
285286 } else {
286287 " "
287288 }
289+ val sshOpts = """
290+ ConnectTimeout 0
291+ StrictHostKeyChecking no
292+ UserKnownHostsFile /dev/null
293+ LogLevel ERROR
294+ SetEnv CODER_SSH_SESSION_TYPE=JetBrains
295+ """ .trimIndent()
288296 val blockContent =
297+ if (feats.wildcardSSH) {
298+ startBlock + System .lineSeparator() +
299+ """
300+ Host ${getHostPrefix()} --*
301+ ProxyCommand ${proxyArgs.joinToString(" " )} --ssh-host-prefix ${getHostPrefix()} -- %h
302+ """ .trimIndent()
303+ .plus(" \n " + sshOpts.prependIndent(" " ))
304+ .plus(extraConfig)
305+ .plus(" \n\n " )
306+ .plus(
307+ """
308+ Host ${getHostPrefix()} -bg--*
309+ ProxyCommand ${backgroundProxyArgs.joinToString(" " )} --ssh-host-prefix ${getHostPrefix()} -bg-- %h
310+ """ .trimIndent()
311+ .plus(" \n " + sshOpts.prependIndent(" " ))
312+ .plus(extraConfig),
313+ ).replace(" \n " , System .lineSeparator()) +
314+ System .lineSeparator() + endBlock
315+
316+ } else {
289317 workspaceNames.joinToString(
290318 System .lineSeparator(),
291319 startBlock + System .lineSeparator(),
292320 System .lineSeparator() + endBlock,
293321 transform = {
294322 """
295- Host ${getHostName(deploymentURL, it.first, currentUser, it.second)}
323+ Host ${getHostName(it.first, currentUser, it.second)}
296324 ProxyCommand ${proxyArgs.joinToString(" " )} ${getWorkspaceParts(it.first, it.second)}
297- ConnectTimeout 0
298- StrictHostKeyChecking no
299- UserKnownHostsFile /dev/null
300- LogLevel ERROR
301- SetEnv CODER_SSH_SESSION_TYPE=JetBrains
302325 """ .trimIndent()
326+ .plus(" \n " + sshOpts.prependIndent(" " ))
303327 .plus(extraConfig)
304328 .plus(" \n " )
305329 .plus(
306330 """
307- Host ${getBackgroundHostName(deploymentURL, it.first, currentUser, it.second)}
331+ Host ${getBackgroundHostName(it.first, currentUser, it.second)}
308332 ProxyCommand ${backgroundProxyArgs.joinToString(" " )} ${getWorkspaceParts(it.first, it.second)}
309- ConnectTimeout 0
310- StrictHostKeyChecking no
311- UserKnownHostsFile /dev/null
312- LogLevel ERROR
313- SetEnv CODER_SSH_SESSION_TYPE=JetBrains
314333 """ .trimIndent()
334+ .plus(" \n " + sshOpts.prependIndent(" " ))
315335 .plus(extraConfig),
316336 ).replace(" \n " , System .lineSeparator())
317337 },
318338 )
339+ }
319340
320341 if (contents == null ) {
321342 logger.info(" No existing SSH config to modify" )
@@ -489,40 +510,53 @@ class CoderCLIManager(
489510 Features (
490511 disableAutostart = version >= SemVer (2 , 5 , 0 ),
491512 reportWorkspaceUsage = version >= SemVer (2 , 13 , 0 ),
513+ wildcardSSH = version >= SemVer (2 , 19 , 0 ),
492514 )
493515 }
494516 }
495517
518+ /*
519+ * This function returns the ssh-host-prefix used for Host entries.
520+ */
521+ fun getHostPrefix (): String =
522+ " coder-jetbrains-${deploymentURL.safeHost()} "
523+
524+ /* *
525+ * This function returns the ssh host name generated for connecting to the workspace.
526+ */
527+ fun getHostName (
528+ workspace : Workspace ,
529+ currentUser : User ,
530+ agent : WorkspaceAgent ,
531+ ): String =
532+ if (features.wildcardSSH) {
533+ " ${getHostPrefix()} --${workspace.ownerName} --${workspace.name} .${agent.name} "
534+ } else {
535+ // For a user's own workspace, we use the old syntax without a username for backwards compatibility,
536+ // since the user might have recent connections that still use the old syntax.
537+ if (currentUser.username == workspace.ownerName) {
538+ " coder-jetbrains--${workspace.name} .${agent.name} --${deploymentURL.safeHost()} "
539+ } else {
540+ " coder-jetbrains--${workspace.ownerName} --${workspace.name} .${agent.name} --${deploymentURL.safeHost()} "
541+ }
542+ }
543+
544+ fun getBackgroundHostName (
545+ workspace : Workspace ,
546+ currentUser : User ,
547+ agent : WorkspaceAgent ,
548+ ): String =
549+ if (features.wildcardSSH) {
550+ " ${getHostPrefix()} -bg--${workspace.ownerName} --${workspace.name} .${agent.name} "
551+ } else {
552+ getHostName(workspace, currentUser, agent) + " --bg"
553+ }
554+
496555 companion object {
497556 val logger = Logger .getInstance(CoderCLIManager ::class .java.simpleName)
498557
499558 private val tokenRegex = " --token [^ ]+" .toRegex()
500559
501- /* *
502- * This function returns the ssh host name generated for connecting to the workspace.
503- */
504- @JvmStatic
505- fun getHostName (
506- url : URL ,
507- workspace : Workspace ,
508- currentUser : User ,
509- agent : WorkspaceAgent ,
510- ): String =
511- // For a user's own workspace, we use the old syntax without a username for backwards compatibility,
512- // since the user might have recent connections that still use the old syntax.
513- if (currentUser.username == workspace.ownerName) {
514- " coder-jetbrains--${workspace.name} .${agent.name} --${url.safeHost()} "
515- } else {
516- " coder-jetbrains--${workspace.ownerName} --${workspace.name} .${agent.name} --${url.safeHost()} "
517- }
518-
519- fun getBackgroundHostName (
520- url : URL ,
521- workspace : Workspace ,
522- currentUser : User ,
523- agent : WorkspaceAgent ,
524- ): String = getHostName(url, workspace, currentUser, agent) + " --bg"
525-
526560 /* *
527561 * This function returns the identifier for the workspace to pass to the
528562 * coder ssh proxy command.
@@ -536,6 +570,18 @@ class CoderCLIManager(
536570 @JvmStatic
537571 fun getBackgroundHostName (
538572 hostname : String ,
539- ): String = hostname + " --bg"
573+ ): String {
574+ val parts = hostname.split(" --" ).toMutableList()
575+ if (parts.size < 2 ) {
576+ throw SSHConfigFormatException (" Invalid hostname: $hostname " )
577+ }
578+ // non-wildcard case
579+ if (parts[0 ] == " coder-jetbrains" ) {
580+ return hostname + " --bg"
581+ }
582+ // wildcard case
583+ parts[0 ] + = " -bg"
584+ return parts.joinToString(" --" )
585+ }
540586 }
541587}
0 commit comments