Skip to content

Commit

Permalink
Fix 'not recognized as an internal or external command' for dev envs …
Browse files Browse the repository at this point in the history
…on windows (#3853)
  • Loading branch information
rli authored Sep 7, 2023
1 parent 773a115 commit ad1c5a0
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Fix 'not recognzied as an ... command' when connecting to CodeCatalyst Dev Environments on Windows"
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ abstract class AbstractSsmCommandExecutor(private val region: AwsRegion, protect

private fun newSshCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).sshCommand()

fun proxyCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).generateProxyCommand()
fun proxyCommand() = SsmCommandLineFactory(ssmTarget, startSsh(), region).proxyCommand()

private companion object {
private val LOG = getLogger<AbstractSsmCommandExecutor>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

package software.aws.toolkits.jetbrains.gateway.connection

import com.intellij.execution.CommandLineUtil
import com.intellij.execution.Platform
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.tools.ToolManager
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin

Expand All @@ -23,56 +24,59 @@ class SsmCommandLineFactory(
private val ssmTarget: String,
private val sessionParameters: StartSessionResponse,
private val region: AwsRegion,
private val overrideSsmPlugin: String? = null,
private val overrideWindowsWrapper: String? = null
private val overrideSsmPlugin: String? = null
) {
fun sshCommand(): SshCommandLine {
val command = SshCommandLine(ssmTarget)
val proxyCommand = generateProxyCommand()
command.addSshOption("-o", "ProxyCommand=${proxyCommand.commandString}")
command.addSshOption("-o", "ProxyCommand=${proxyCommand()}")
command.addSshOption("-o", "ServerAliveInterval=60")
command.withEnvironment(proxyCommand.environment)

return command
}

fun scpCommand(remotePath: String, recursive: Boolean = false): ScpCommandLine {
val command = ScpCommandLine(ssmTarget, remotePath, recursive)
val proxyCommand = generateProxyCommand()
command.addSshOption("-o", "ProxyCommand=${proxyCommand.commandString}")
command.withEnvironment(proxyCommand.environment)
command.addSshOption("-o", "ProxyCommand=${proxyCommand()}")

return command
}

/**
* This returns a GeneralCommandLine is meant to be executed directly.
* Use [proxyCommand] instead if you need a value for the SSH "ProxyCommand" property
*/
fun rawCommand(): GeneralCommandLine = generateProxyCommand().let {
GeneralCommandLine(it.exePath)
.withEnvironment(it.environment)
.apply {
if (it.args != null) {
withParameters(it.args)
}
}
.withParameters(it.args)
}

inner class ProxyCommand(
val exePath: String,
val ssmPayload: String? = null,
val environment: Map<String, String> = emptyMap()
) {
val commandString by lazy {
val args: List<String>
)

/**
* This is meant to be passed directly as a value into the SSH "ProxyCommand" property
* Use [rawCommand] instead for command execution
*/
fun proxyCommand(): String {
val rawCommand = rawCommand()

return if (SystemInfo.isWindows) {
// see [GeneralCommandLine.getPreparedCommandLine]
CommandLineUtil.toCommandLine(rawCommand.exePath, rawCommand.parametersList.list, Platform.current()).joinToString(separator = " ")
} else {
// on *nix, the quoting on getPreparedCommandLine is not quite correct since the arguments aren't being passed directly to execv
buildString {
append(exePath)
if (ssmPayload != null) {
append(""" '$ssmPayload' ${region.id} StartSession""")
append(rawCommand.exePath)
rawCommand.parametersList.list.forEach {
append(" '$it'")
}
}
}

val args = ssmPayload?.let { listOf(it, region.id, "StartSession") }
}

fun generateProxyCommand(): ProxyCommand {
private fun generateProxyCommand(): ProxyCommand {
val ssmPluginJson = """
{
"streamUrl":"${sessionParameters.streamUrl}",
Expand All @@ -84,21 +88,9 @@ class SsmCommandLineFactory(
val ssmPath = overrideSsmPlugin
?: ToolManager.getInstance().getOrInstallTool(SsmPlugin).path.toAbsolutePath().toString()

return if (SystemInfo.isWindows) {
ProxyCommand(
exePath = overrideWindowsWrapper
?: AwsToolkit.pluginPath().resolve("gateway-resources").resolve("caws-proxy-command.bat").toAbsolutePath().toString(),
environment = mapOf(
"sessionManagerExe" to ssmPath,
"sessionManagerJson" to '"' + ssmPluginJson.replace("\"", "\\\"") + '"',
"region" to region.id
)
)
} else {
ProxyCommand(
exePath = ssmPath,
ssmPayload = ssmPluginJson
)
}
return ProxyCommand(
ssmPath,
listOf(ssmPluginJson, region.id, "StartSession")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CawsCommandExecutor(
ssmTarget: String,
private val spaceName: String,
private val projectName: String
) : AbstractSsmCommandExecutor(REGION, ssmTarget) {
) : AbstractSsmCommandExecutor(AwsRegion.GLOBAL, ssmTarget) {
override fun startSsh(): StartSessionResponse =
startSession {
it.sessionConfiguration { session ->
Expand Down Expand Up @@ -49,9 +49,4 @@ class CawsCommandExecutor(
tokenValue = session.accessDetails().tokenValue()
)
}

companion object {
// TODO: devWorkspace APIs are only in us-west-2 at the moment
private val REGION = AwsRegion("us-west-2", "us-west-2", "aws")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.intellij.ssh.config.SshConnectionConfigService
import com.intellij.ssh.config.SshProxyConfig
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.gateway.connection.AbstractSsmCommandExecutor

class CawsSshConnectionConfigModifier : SshConnectionConfigService.Modifier {
override fun modify(initialHost: String, connectionConfig: SshConnectionConfig): SshConnectionConfig {
Expand All @@ -24,13 +25,16 @@ class CawsSshConnectionConfigModifier : SshConnectionConfigService.Modifier {
projectName = project
)

return connectionConfig.copy(
proxyConfig = SshProxyConfig.Command(executor.proxyCommand().commandString),
hostKeyVerifier = PromiscuousSshHostKeyVerifier
)
return modify(executor, connectionConfig)
}

companion object {
const val HOST_PREFIX = "aws.codecatalyst:"

fun modify(executor: AbstractSsmCommandExecutor, connectionConfig: SshConnectionConfig): SshConnectionConfig =
connectionConfig.copy(
proxyConfig = SshProxyConfig.Command(executor.proxyCommand()),
hostKeyVerifier = PromiscuousSshHostKeyVerifier
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class StartBackendV2(
override val stepName: String = message("gateway.connection.workflow.start_ide")

override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
stepEmitter.emitMessageLine("Waiting for IDE to start on Dev Environment. See Gateway logs for details.", false)

val creds = RemoteCredentialsHolder().apply {
setHost("${CawsSshConnectionConfigModifier.HOST_PREFIX}${identifier.friendlyString}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,13 @@ class SsmCommandLineTest {
"target",
StartSessionResponse("session", "stream", "token"),
AwsRegion.GLOBAL,
overrideSsmPlugin = "session manager plugin",
overrideWindowsWrapper = "session manager plugin"
overrideSsmPlugin = "session manager plugin"
).sshCommand()

if (SystemInfo.isWindows) {
assertThat(sut.constructCommandLine().commandLineString).matches(
"""
(.*)?-o "ProxyCommand=session manager plugin"(.*)?
(.*)?-o "ProxyCommand=session manager plugin (.*)?"(.*)?
""".trimIndent().toPattern()
)
} else {
Expand All @@ -65,7 +64,7 @@ class SsmCommandLineTest {
val sut = sutFactory.sshCommand()
assertThat(sut.constructCommandLine().commandLineString).matches(
"""
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' aws-global StartSession" -o ServerAliveInterval=60
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' 'aws-global' 'StartSession'" -o ServerAliveInterval=60
""".trimIndent().toPattern()
)
}
Expand All @@ -76,18 +75,9 @@ class SsmCommandLineTest {
val sut = sutFactory.sshCommand()
assertThat(sut.constructCommandLine().commandLineString).matches(
"""
$prefix -o ProxyCommand=(.*)?caws-proxy-command.bat -o ServerAliveInterval=60
$prefix -o "ProxyCommand=session-manager-plugin \\"\{\\\\"streamUrl\\\\":\\\\"stream\\\\",\\\\"tokenValue\\\\":\\\\"token\\\\",\\\\"sessionId\\\\":\\\\"session\\\\"}\\" aws-global StartSession" -o ServerAliveInterval=60
""".trimIndent().toPattern()
)

assertThat(sut.constructCommandLine().effectiveEnvironment)
.containsAllEntriesOf(
mapOf(
"sessionManagerExe" to "session-manager-plugin",
"sessionManagerJson" to """"{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}"""",
"region" to "aws-global"
)
)
}

@Test
Expand All @@ -98,7 +88,7 @@ class SsmCommandLineTest {

assertThat(sut.constructCommandLine().commandLineString).matches(
"""
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' aws-global StartSession" localPath target:remote
$prefix -o "ProxyCommand=session-manager-plugin '\{\\"streamUrl\\":\\"stream\\",\\"tokenValue\\":\\"token\\",\\"sessionId\\":\\"session\\"}' 'aws-global' 'StartSession'" localPath target:remote
""".trimIndent().toPattern()
)
}
Expand All @@ -111,17 +101,8 @@ class SsmCommandLineTest {

assertThat(sut.constructCommandLine().commandLineString).matches(
"""
$prefix -o ProxyCommand=(.*)?caws-proxy-command.bat localPath target:remote
$prefix-o "ProxyCommand=session-manager-plugin \\"\{\\\\"streamUrl\\\\":\\\\"stream\\\\",\\\\"tokenValue\\\\":\\\\"token\\\\",\\\\"sessionId\\\\":\\\\"session\\\\"}\\" aws-global StartSession" localPath target:remote
""".trimIndent().toPattern()
)

assertThat(sut.constructCommandLine().effectiveEnvironment)
.containsAllEntriesOf(
mapOf(
"sessionManagerExe" to "session-manager-plugin",
"sessionManagerJson" to """"{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}"""",
"region" to "aws-global"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.gateway.connection.caws

import com.intellij.openapi.util.SystemInfo
import com.intellij.ssh.PromiscuousSshHostKeyVerifier
import com.intellij.ssh.config.SshConnectionConfig
import com.intellij.ssh.config.SshProxyConfig
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.tools.MockToolManagerRule
import software.aws.toolkits.jetbrains.core.tools.Tool
import software.aws.toolkits.jetbrains.gateway.connection.AbstractSsmCommandExecutor
import software.aws.toolkits.jetbrains.gateway.connection.StartSessionResponse
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
import java.nio.file.Path

class CawsSshConnectionConfigModifierTest {
@Rule
@JvmField
val toolManager = MockToolManagerRule()

@Test
fun `modify only mutates CodeCatalyst targets`() {
val initial = SshConnectionConfig("test")
val sut = CawsSshConnectionConfigModifier()

assertThat(sut.modify(initial.host, initial)).isEqualTo(initial)
}

@Test
fun `modify adds proxy command to CodeCatalyst targets`() {
val dummyExecutor = object : AbstractSsmCommandExecutor(AwsRegion.GLOBAL, "test") {
val response = StartSessionResponse("session", "stream", "token")

override fun startSsh() = response
override fun startSsm(exe: String, vararg args: String) = response
}
val mockPath = Path.of("ssm")
val mockTool = mock<Tool<SsmPlugin>> {
on {
path
}.doReturn(mockPath)
}

toolManager.registerTool(SsmPlugin, mockTool)

val proxyCommand = if (SystemInfo.isWindows) {
"""${mockPath.toAbsolutePath()} "{\"streamUrl\":\"stream\",\"tokenValue\":\"token\",\"sessionId\":\"session\"}" aws-global StartSession"""
} else {
"""${mockPath.toAbsolutePath()} '{"streamUrl":"stream","tokenValue":"token","sessionId":"session"}' 'aws-global' 'StartSession'"""
}

assertThat(CawsSshConnectionConfigModifier.modify(dummyExecutor, SshConnectionConfig("test")))
.isEqualTo(
SshConnectionConfig("test").copy(
proxyConfig = SshProxyConfig.Command(command = proxyCommand),
hostKeyVerifier = PromiscuousSshHostKeyVerifier
)
)
}
}

0 comments on commit ad1c5a0

Please sign in to comment.