// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.gateway.connection.workflow import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.ide.plugins.PluginManager import com.intellij.openapi.util.text.StringUtil import com.intellij.util.io.Compressor import com.intellij.util.io.DigestUtil import com.intellij.util.io.HttpRequests import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.presigner.S3Presigner import software.aws.toolkits.core.ConnectionSettings import software.aws.toolkits.core.utils.exists import software.aws.toolkits.jetbrains.core.awsClient import software.aws.toolkits.jetbrains.core.credentials.CredentialManager import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider import software.aws.toolkits.jetbrains.gateway.GatewayProduct import software.aws.toolkits.jetbrains.gateway.ToolkitInstallSettings import software.aws.toolkits.jetbrains.gateway.connection.AbstractSsmCommandExecutor import software.aws.toolkits.jetbrains.gateway.connection.GET_IDE_BACKEND_VERSION_COMMAND import software.aws.toolkits.jetbrains.utils.execution.steps.CliBasedStep import software.aws.toolkits.jetbrains.utils.execution.steps.Context import software.aws.toolkits.jetbrains.utils.execution.steps.Step import software.aws.toolkits.jetbrains.utils.execution.steps.StepWorkflow import software.aws.toolkits.resources.message import java.io.File import java.nio.file.Path import java.time.Duration import java.util.UUID abstract class InstallPluginBackend( protected val commandExecutor: AbstractSsmCommandExecutor, protected val remoteScriptPath: String, protected val idePath: String ) : CliBasedStep() { override val stepName: String = message("gateway.connection.workflow.install_toolkit") override fun constructCommandLine(context: Context): GeneralCommandLine? { val url = buildDownloadUrl() ?: return null val cmd = """$remoteScriptPath/install-plugin.sh "${idePath.trimEnd('/')}" "$url"""" return commandExecutor.buildSshCommand { it.addToRemoteCommand(cmd) } } protected abstract fun buildDownloadUrl(): String? class InstallMarketplacePluginBackend( private val ideProduct: GatewayProduct?, commandExecutor: AbstractSsmCommandExecutor, remoteScriptPath: String, idePath: String, private val marketplaceUrl: String = "https://plugins.jetbrains.com" ) : InstallPluginBackend(commandExecutor, remoteScriptPath, idePath) { override fun buildDownloadUrl(): String? { val baseUrl = "$marketplaceUrl/pluginManager?action=download&id=aws.toolkit&build=" if (ideProduct != null) { val url = "$baseUrl${ideProduct.productCode}-${ideProduct.buildNumber}" val responseCode = HttpRequests.head(url).throwStatusCodeException(false).tryConnect() return if (responseCode in 200..299) { url } else { null } } return "$baseUrl$($GET_IDE_BACKEND_VERSION_COMMAND)" } } class InstallLocalPluginBackend( private val installSettings: ToolkitInstallSettings.UseArbitraryLocalPath, commandExecutor: AbstractSsmCommandExecutor, remoteScriptPath: String, idePath: String ) : InstallPluginBackend(commandExecutor, remoteScriptPath, idePath) { override fun buildDownloadUrl(): String? { val credId = CredentialManager.getInstance().getCredentialIdentifierById("profile:default") ?: error("Default profile not available") val region = AwsRegionProvider.getInstance().defaultRegion() val connectionSettings = ConnectionSettings(CredentialManager.getInstance().getAwsCredentialProvider(credId, region), region) val toolkitPath = Path.of(installSettings.localToolkitPath) if (!toolkitPath.exists()) { return null } val messageDigest = DigestUtil.md5() DigestUtil.updateContentHash(messageDigest, toolkitPath) val toolkitHash = StringUtil.toHexString(messageDigest.digest()) val s3Client = connectionSettings.awsClient<S3Client>() val s3StagingBucket = installSettings.s3StagingBucket val s3Key = toolkitPath.fileName.toString() try { // Will throw if the ETag doesn't match, so it will re-upload s3Client.headObject { it.bucket(s3StagingBucket) it.key(s3Key) it.ifMatch(toolkitHash) } } catch (e: Exception) { s3Client.putObject( { it.bucket(s3StagingBucket) it.key(s3Key) }, toolkitPath ) } val s3Presigner = S3Presigner.builder().credentialsProvider(connectionSettings.credentials).region(Region.of(connectionSettings.region.id)).build() val presignGetObject = s3Presigner.presignGetObject { it.signatureDuration(Duration.ofMinutes(10)) it.getObjectRequest { req -> req.bucket(s3StagingBucket) req.key(s3Key) } } return presignGetObject.url().toString() } } } fun installBundledPluginBackend( commandExecutor: AbstractSsmCommandExecutor, remoteScriptPath: String, idePath: String ): Step { val remotePluginPath = "/tmp/${UUID.randomUUID()}.zip" return object : StepWorkflow( ZipAndCopyBundledPlugin(remotePluginPath, commandExecutor), InstallBundledPluginBackend(remotePluginPath, commandExecutor, remoteScriptPath, idePath) ) { override val stepName = "Install pre-GA development build (this will take a while)" } } private class ZipAndCopyBundledPlugin( private val remotePluginPath: String, private val commandExecutor: AbstractSsmCommandExecutor ) : CliBasedStep() { override val stepName: String = "Zip and copy bundled plugin" override fun constructCommandLine(context: Context): GeneralCommandLine? { val pluginPath = PluginManager.getPluginByClass(InstallBundledPluginBackend::class.java)?.pluginPath ?: throw RuntimeException("Could not determine AWS Toolkit plugin path") val zipPath = File.createTempFile("toolkit", "zip") val zip = Compressor.Zip(zipPath) zip.use { it.addDirectory(pluginPath.fileName.toString(), pluginPath) } return commandExecutor.buildScpCommand(remotePluginPath, false, zipPath.toPath()) } } private class InstallBundledPluginBackend( private val remotePluginPath: String, commandExecutor: AbstractSsmCommandExecutor, remoteScriptPath: String, idePath: String ) : InstallPluginBackend(commandExecutor, remoteScriptPath, idePath) { override fun buildDownloadUrl(): String? = "file://$remotePluginPath" }