diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d2e89e8..6a45304 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,9 +19,6 @@ jobs: build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - - name: Download latest Infracost - run: ./scripts/download.sh - - name: Run build plugin run: ./gradlew buildPlugin diff --git a/CHANGELOG.md b/CHANGELOG.md index 7641cc0..8a7ff82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,27 +4,44 @@ ## [Unreleased] +## [1.2.0] - 2024-07-09 + +### Changed + +- Download the latest Infracost binary on plugin rather than bundling it + ## [1.1.1] - 2024-07-04 + ### Fixed + - Limit subsequent runs to one queued run, this handles large scale saves and button mashing ### Changed + - Set the platform identifier to jetbrains ## [1.1.0] - 2024-07-02 + ### Added + - Support for config files - Support for usage files ## [1.0.2] - 2024-07-01 + ### Fixed + - Update linux binaries path - Make the "connect to Infracost" link more simple ## [1.0.1] - 2024-07-01 + ### Fixed + - Updates to readme ## [1.0.0] - 2024-06-29 + ### Added + - Initial release of the Infracost plugin for JetBrains IDEs. \ No newline at end of file diff --git a/README.md b/README.md index efc911e..7a72d7c 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ ## Description -Infracost is a JetBrains IDE plugin that allows you to shift left on your Cloud costs by providing cost estimates for your Terraform code. +Infracost is a JetBrains IDE plugin that allows you to shift left on your Cloud costs by providing cost estimates for +your Terraform code. -Infracost is a companion to the [Infracost CLI](https://www.infracost.io/docs/integrations/ci-cd) and provides a way to view cost estimates directly in your IDE. +Infracost is a companion to the [Infracost CLI](https://www.infracost.io/docs/integrations/ci-cd) and provides a way to +view cost estimates directly in your IDE. ## Features @@ -30,4 +32,6 @@ You can install the plugin from the JetBrains Plugin Repository. 4. Restart the IDE 5. Open a Terraform file and click on the `Infracost` tab at the bottom of the IDE 6. Click on `Refresh` to get the cost estimate -7. Use our ![CI/CD integrations](https://www.infracost.io/docs/integrations/cicd/) to add cost estimates to pull requests. This provides your team with a safety net as people can understand cloud costs upfront, and discuss them as part of your workflow. +7. Use our ![CI/CD integrations](https://www.infracost.io/docs/integrations/cicd/) to add cost estimates to pull + requests. This provides your team with a safety net as people can understand cloud costs upfront, and discuss them as + part of your workflow. diff --git a/build.gradle.kts b/build.gradle.kts index 5848217..45b3e41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,7 @@ repositories { } dependencies { + implementation("org.apache.commons:commons-compress:1.21") } kotlin { @@ -82,10 +83,10 @@ tasks { changeNotes = properties("pluginVersion").map { pluginVersion -> with(changelog) { renderItem( - (getOrNull(pluginVersion) ?: getUnreleased()) - .withHeader(false) - .withEmptySections(false), - Changelog.OutputType.HTML, + (getOrNull(pluginVersion) ?: getUnreleased()) + .withHeader(false) + .withEmptySections(false), + Changelog.OutputType.HTML, ) } } @@ -109,6 +110,9 @@ tasks { publishPlugin { dependsOn("patchChangelog", "signPlugin") token = environment("PUBLISH_TOKEN") - channels = properties("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } + channels = properties("pluginVersion").map { + listOf( + it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) + } } } diff --git a/gradle.properties b/gradle.properties index e45597a..2370d31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,14 @@ # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html - -pluginGroup = io.infracost.plugins -pluginName = Infracost -pluginRepositoryUrl = https://github.com/infracost/jetbrains-infracost -pluginVersion = 1.1.1 - -pluginSinceBuild = 232 -pluginUntilBuild = 242.* - -platformType = IC -platformVersion = 2023.2.7 -platformPlugins = -gradleVersion = 8.8 -kotlin.stdlib.default.dependency = false -org.gradle.configuration-cache = true -org.gradle.caching = true \ No newline at end of file +pluginGroup=io.infracost.plugins +pluginName=Infracost +pluginRepositoryUrl=https://github.com/infracost/jetbrains-infracost +pluginVersion=1.2.0 +pluginSinceBuild=232 +pluginUntilBuild=242.* +platformType=IC +platformVersion=2023.2.7 +platformPlugins= +gradleVersion=8.8 +kotlin.stdlib.default.dependency=false +org.gradle.configuration-cache=true +org.gradle.caching=true \ No newline at end of file diff --git a/scripts/download.sh b/scripts/download.sh deleted file mode 100755 index a479bfd..0000000 --- a/scripts/download.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env sh -# This script is used in the README and https://www.infracost.io/docs/#quick-start -set -e - -# check_sha is separated into a defined function so that we can -# capture the exit code effectively with `set -e` enabled -check_sha() { - ( - cd /tmp/ - shasum -sc "$1" - ) - return $? -} - -download_binary() { - bin_target=$1 - resource_path=$2 - - if [ -z "$bin_target" ]; then - echo "Please provide the target binary to download" - exit 1 - fi - - if [ -z "$resource_path" ]; then - echo "Please provide the resource path to download the binary to" - exit 1 - fi - - url="https://infracost.io/downloads/latest" - tar="infracost-$bin_target.tar.gz" - echo "Downloading latest release of infracost-$bin_target..." - curl -sL "$url/$tar" -o "/tmp/$tar" - echo - - code=$(curl -s -L -o /dev/null -w "%{http_code}" "$url/$tar.sha256") - if [ "$code" = "404" ]; then - echo "Skipping checksum validation as the sha for the release could not be found, no action needed." - else - echo "Validating checksum for infracost-$bin_target..." - curl -sL "$url/$tar.sha256" -o "/tmp/$tar.sha256" - - if ! check_sha "$tar.sha256"; then - exit 1 - fi - - rm "/tmp/$tar.sha256" - fi - echo - - tar xzf "/tmp/$tar" -C /tmp - rm "/tmp/$tar" - - # shellcheck disable=SC2046 - mkdir -p $(dirname "${resource_path}") - - if echo "$bin_target" | grep "windows-arm"; then - mv "/tmp/infracost-arm64.exe" "${resource_path}" - elif echo "$bin_target" | grep "windows"; then - mv "/tmp/infracost.exe" "${resource_path}" - else - mv "/tmp/infracost-$bin_target" "${resource_path}" - fi -} - -download_binary "darwin-amd64" "src/main/resources/binaries/macos/amd64/infracost" -download_binary "darwin-arm64" "src/main/resources/binaries/macos/aarch64/infracost" -download_binary "linux-amd64" "src/main/resources/binaries/linux/amd64/infracost" -download_binary "linux-arm64" "src/main/resources/binaries/linux/aarch64/infracost" -download_binary "windows-arm64" "src/main/resources/binaries/windows/aarch64/infracost.exe" -download_binary "windows-amd64" "src/main/resources/binaries/windows/amd64/infracost.exe" diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/DownloadInfracostAction.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/DownloadInfracostAction.kt new file mode 100644 index 0000000..5db8585 --- /dev/null +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/DownloadInfracostAction.kt @@ -0,0 +1,28 @@ +package io.infracost.plugins.infracost.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import io.infracost.plugins.infracost.actions.tasks.InfracostDownloadBinaryTask +import javax.swing.SwingUtilities + +class DownloadInfracostAction : AnAction() { + + override fun actionPerformed(e: AnActionEvent) { + ProgressManager.getInstance().run(InfracostDownloadBinaryTask(e.project!!, false)) + } + + companion object { + fun runDownload(project: Project, initial: Boolean = false) { + val runner = + InfracostDownloadBinaryTask(project, initial) + if (SwingUtilities.isEventDispatchThread()) { + ProgressManager.getInstance().run(runner) + } else { + ApplicationManager.getApplication().invokeLater(runner) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/RunInfracostAction.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/RunInfracostAction.kt index d45b208..9eef8d4 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/actions/RunInfracostAction.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/RunInfracostAction.kt @@ -64,8 +64,8 @@ class RunInfracostAction : AnAction() { } if (running.get()) { - // If a run is already in progress, queue the next run - next.set(runner) + // If a run is already in progress, queue the next run if not already queued + next.compareAndSet(null, runner) return } diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostAuthRunTask.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostAuthRunTask.kt index e91e71a..27cce50 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostAuthRunTask.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostAuthRunTask.kt @@ -1,11 +1,12 @@ package io.infracost.plugins.infracost.actions.tasks import com.intellij.execution.ExecutionException -import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project import io.infracost.plugins.infracost.actions.CheckAuthAction +import io.infracost.plugins.infracost.binary.InfracostBinary import io.infracost.plugins.infracost.ui.notify.InfracostNotificationGroup import java.io.BufferedReader import java.io.InputStreamReader @@ -15,36 +16,27 @@ import javax.swing.SwingUtilities internal class InfracostAuthRunTask( private val project: Project, private val callback: Consumer -) : InfracostTask(project, "Authenticate Infracost", false), Runnable { +) : Backgroundable(project, "Authenticate Infracost", false), Runnable { override fun run(indicator: ProgressIndicator) { this.run() } override fun run() { - if (!ensureBinaryAvailable()) { - InfracostNotificationGroup.notifyError(project, "Infracost binary not found") - return - } + val commandParts: MutableList = ArrayList() + commandParts.add(InfracostBinary.binaryFile) + commandParts.add("auth") + commandParts.add("login") - val commandParams: MutableList = ArrayList() - commandParams.add(binaryFile) - commandParams.add("auth") - commandParams.add("login") + val command = ProcessBuilder(commandParts) + command.environment().set("INFRACOST_CLI_PLATFORM", "jetbrains") + command.environment().set("INFRACOST_SKIP_UPDATE_CHECK", "true") + command.environment().set("INFRACOST_GRAPH_EVALUATOR", "true") + command.environment().set("INFRACOST_NO_COLOR", "true") - val commandLine = - GeneralCommandLine(commandParams) - .withEnvironment( - mapOf( - "INFRACOST_SKIP_UPDATE_CHECK" to "true", - "INFRACOST_GRAPH_EVALUATOR" to "true", - "INFRACOST_NO_COLOR" to "true", - "INFRACOST_CLI_PLATFORM" to "jetbrains", - ) - ) ApplicationManager.getApplication().executeOnPooledThread { try { try { - val process = Runtime.getRuntime().exec(commandLine.commandLineString) + val process = command.start() val inputReader = BufferedReader(InputStreamReader(process.inputStream)) val inputThread = Thread { try { diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostBackgroundRunTask.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostBackgroundRunTask.kt index 64cb5b2..d1c6383 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostBackgroundRunTask.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostBackgroundRunTask.kt @@ -1,11 +1,12 @@ package io.infracost.plugins.infracost.actions.tasks import com.intellij.execution.ExecutionException -import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.OSProcessHandler import com.intellij.execution.process.ScriptRunnerUtil import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project +import io.infracost.plugins.infracost.binary.InfracostBinary import io.infracost.plugins.infracost.ui.notify.InfracostNotificationGroup import java.io.File import java.nio.file.Paths @@ -16,25 +17,19 @@ internal class InfracostBackgroundRunTask( private val project: Project, private val resultFile: File, private val callback: BiConsumer -) : InfracostTask(project, "Running Infracost", false), Runnable { +) : Backgroundable(project, "Running Infracost", false), Runnable { override fun run(indicator: ProgressIndicator) { this.run() } override fun run() { - if (!ensureBinaryAvailable()) { - InfracostNotificationGroup.notifyError(project, "Infracost binary not found") - return - } - val infracostConfigPath = Paths.get(project.basePath, "infracost.yml") val infracostConfigTemplathPath = Paths.get(project.basePath, "infracost.yml.tmpl") val infracostUsageFilePath = Paths.get(project.basePath, "infracost-usage.yml") - val commandParts: MutableList = ArrayList() - commandParts.add(binaryFile) + commandParts.add(InfracostBinary.binaryFile) commandParts.add("breakdown") commandParts.add("--format=json") commandParts.add(String.format("--out-file=%s", resultFile.absolutePath)) @@ -52,27 +47,21 @@ internal class InfracostBackgroundRunTask( } } - val commandLine = - GeneralCommandLine(commandParts) - .withEnvironment( - mapOf( - "INFRACOST_CLI_PLATFORM" to "jetbrains", - "INFRACOST_SKIP_UPDATE_CHECK" to "true", - "INFRACOST_GRAPH_EVALUATOR" to "true", - "INFRACOST_NO_COLOR" to "true" - ) - ) - - commandLine.setWorkDirectory(project.basePath) + val command = ProcessBuilder(commandParts) + command.environment().set("INFRACOST_CLI_PLATFORM", "jetbrains") + command.environment().set("INFRACOST_SKIP_UPDATE_CHECK", "true") + command.environment().set("INFRACOST_GRAPH_EVALUATOR", "true") + command.environment().set("INFRACOST_NO_COLOR", "true") + command.directory(File(project.basePath.toString())) val process: Process try { - process = commandLine.createProcess() + process = command.start() } catch (e: ExecutionException) { InfracostNotificationGroup.notifyError(project, e.localizedMessage) return } - val handler = OSProcessHandler(process, commandLine.commandLineString) + val handler = OSProcessHandler(process, command.toString()) try { ScriptRunnerUtil.getProcessOutput( handler, ScriptRunnerUtil.STDOUT_OR_STDERR_OUTPUT_KEY_FILTER, 100000000 diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostCheckAuthRunTask.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostCheckAuthRunTask.kt index 7f87bc5..5931e6e 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostCheckAuthRunTask.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostCheckAuthRunTask.kt @@ -1,52 +1,56 @@ package io.infracost.plugins.infracost.actions.tasks import com.intellij.execution.ExecutionException -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.ScriptRunnerUtil import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project +import io.infracost.plugins.infracost.binary.InfracostBinary import io.infracost.plugins.infracost.ui.notify.InfracostNotificationGroup +import java.io.BufferedReader +import java.io.InputStreamReader import java.util.function.Consumer import javax.swing.SwingUtilities internal class InfracostCheckAuthRunTask( private val project: Project, private val callback: Consumer -) : InfracostTask(project, "Checking Infracost Auth Status", false), Runnable { +) : Backgroundable(project, "Checking Infracost Auth Status", false), Runnable { override fun run(indicator: ProgressIndicator) { this.run() } override fun run() { - if (!ensureBinaryAvailable()) { - InfracostNotificationGroup.notifyError(project, "Infracost binary not found") - return - } + val commandParts: MutableList = ArrayList() + commandParts.add(InfracostBinary.binaryFile) + commandParts.add("configure") + commandParts.add("get") + commandParts.add("api_key") + + try { + val command = ProcessBuilder(commandParts) + command.environment().set("INFRACOST_CLI_PLATFORM", "jetbrains") + command.environment().set("INFRACOST_SKIP_UPDATE_CHECK", "true") + command.environment().set("INFRACOST_GRAPH_EVALUATOR", "true") + command.environment().set("INFRACOST_NO_COLOR", "true") - val commandParams: MutableList = ArrayList() - commandParams.add(binaryFile) - commandParams.add("configure") - commandParams.add("get") - commandParams.add("api_key") + val process = command.start() - val commandLine = - GeneralCommandLine(commandParams) - .withEnvironment( - mapOf( - "INFRACOST_CLI_PLATFORM" to "jetbrains", - "INFRACOST_SKIP_UPDATE_CHECK" to "true", - "INFRACOST_GRAPH_EVALUATOR" to "true", - "INFRACOST_NO_COLOR" to "true" - ) - ) + val inputReader = BufferedReader(InputStreamReader(process.inputStream)) + val inputThread = Thread { + try { + inputReader.forEachLine { line -> + SwingUtilities.invokeLater { callback.accept(line) } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + inputReader.close() + } + } + inputThread.start() - try { - Runtime.getRuntime().exec(commandLine.commandLineString) - val result = - ScriptRunnerUtil.getProcessOutput( - commandLine, ScriptRunnerUtil.STDOUT_OR_STDERR_OUTPUT_KEY_FILTER, 100000000 - ) - SwingUtilities.invokeLater { callback.accept(result) } + // Wait for the process to complete + process.waitFor() } catch (e: ExecutionException) { InfracostNotificationGroup.notifyError(project, e.localizedMessage) } diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostDownloadBinaryTask.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostDownloadBinaryTask.kt new file mode 100644 index 0000000..c8bb728 --- /dev/null +++ b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostDownloadBinaryTask.kt @@ -0,0 +1,103 @@ +package io.infracost.plugins.infracost.actions.tasks + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.project.Project +import io.infracost.plugins.infracost.actions.CheckAuthAction +import io.infracost.plugins.infracost.binary.InfracostBinary +import io.infracost.plugins.infracost.binary.InfracostBinary.Companion.downloadBinary +import io.infracost.plugins.infracost.settings.InfracostSettingState +import java.io.File +import java.nio.file.Paths +import java.util.* +import javax.swing.SwingUtilities + +internal class InfracostDownloadBinaryTask(private val project: Project, val initial: Boolean) : + Backgroundable(project, "Downloading Infracost", false), Runnable { + + + override fun run(indicator: ProgressIndicator) { + this.run() + } + + override fun run() { + if (!initial) { + // this is a force download + getInfracost() + return + } + + if (InfracostSettingState.instance.infracostPath.isEmpty()) { + getInfracost() + return + } else if (File(InfracostSettingState.instance.infracostPath).exists()) { + InfracostBinary.binaryFile = InfracostSettingState.instance.infracostPath + } else { + InfracostBinary.binaryFile = findFileInPath(InfracostSettingState.instance.infracostPath) + if (InfracostBinary.binaryFile.isEmpty()) { + getInfracost() + return + } + } + + SwingUtilities.invokeLater { + CheckAuthAction.checkAuth(project) + } + + } + + private fun findFileInPath(fileName: String): String { + // Get the PATH environment variable + val pathEnv = System.getenv("PATH") ?: return "" + + // Split the PATH into individual directories + val paths = pathEnv.split(File.pathSeparator) + + // Iterate over each directory + for (path in paths) { + val file = File(path, fileName) + // Check if the file exists in this directory + if (file.exists() && file.isFile) { + return file.absolutePath + } + } + return "" + } + + private fun getInfracost() { + + val osName = System.getProperty("os.name").lowercase(Locale.getDefault()) + var arch = System.getProperty("os.arch").lowercase(Locale.getDefault()) + if (arch == "aarch64") { + arch = "arm64" + } + + var binaryTarget = "" + var binaryRelease = "" + if (osName.contains("win")) { + binaryRelease = "windows-${arch}" + binaryTarget = "infracost.exe" + } else if (osName.contains("mac")) { + binaryRelease = "darwin-${arch}" + binaryTarget = "infracost" + } else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) { + binaryRelease = "linux-${arch}" + binaryTarget = "infracost" + } + + PluginManagerCore.getPlugin(PluginId.getId("io.infracost.plugins.jetbrains-infracost"))?.pluginPath?.let { + val targetFile = Paths.get(it.toAbsolutePath().toString(), binaryTarget).toFile() + if (downloadBinary(project, binaryRelease, targetFile, initial)) { + InfracostSettingState.instance.infracostPath = targetFile.absolutePath + InfracostBinary.binaryFile = targetFile.absolutePath + if (initial) { + SwingUtilities.invokeLater { + CheckAuthAction.checkAuth(project) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostTask.kt b/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostTask.kt deleted file mode 100644 index fb05925..0000000 --- a/src/main/kotlin/io/infracost/plugins/infracost/actions/tasks/InfracostTask.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.infracost.plugins.infracost.actions.tasks - -import com.intellij.openapi.progress.Task.Backgroundable -import com.intellij.openapi.project.Project -import io.infracost.plugins.infracost.settings.InfracostSettingState -import java.io.File -import java.io.FileOutputStream -import java.util.* - -abstract class InfracostTask(project: Project, taskTitle: String, cancellable: Boolean) : - Backgroundable(project, taskTitle, cancellable), Runnable { - companion object { - @JvmStatic - protected var binaryFile: String? = null - - fun resetBinaryFile() { - binaryFile = null - } - - fun ensureBinaryAvailable(): Boolean { - if (this.binaryFile != null) { - return true - } - if (InfracostSettingState.instance.infracostPath.isEmpty()) { - ensureBinaryFile() - return this.binaryFile != null - } - - if (File(InfracostSettingState.instance.infracostPath).exists()) { - binaryFile = InfracostSettingState.instance.infracostPath - return true - } - - return findFileInPath(InfracostSettingState.instance.infracostPath) != null - } - - - private fun findFileInPath(fileName: String): String? { - // Get the PATH environment variable - val pathEnv = System.getenv("PATH") ?: return null - - // Split the PATH into individual directories - val paths = pathEnv.split(File.pathSeparator) - - // Iterate over each directory - for (path in paths) { - val file = File(path, fileName) - // Check if the file exists in this directory - if (file.exists() && file.isFile) { - return file.absolutePath - } - } - return null // File not found in any of the directories - } - - private fun ensureBinaryFile() { - val osName = System.getProperty("os.name").lowercase(Locale.getDefault()) - val arch = System.getProperty("os.arch").lowercase(Locale.getDefault()) - - var resourcePath: String? = null - if (osName.contains("win")) { - resourcePath = "/binaries/windows/${arch}/infracost.exe" - } else if (osName.contains("mac")) { - resourcePath = "/binaries/macos/${arch}/infracost" - } else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) { - resourcePath = "/binaries/linux/${arch}/infracost" - } - - if (resourcePath != null) { - val resource = this::class.java.getResourceAsStream(resourcePath) - if (resource != null) { - val file = File.createTempFile("infracost", "") - file.deleteOnExit() - val out = FileOutputStream(file) - out.write(resource.readAllBytes()) - out.close() - file.setExecutable(true) - file.deleteOnExit() - this.binaryFile = file.absolutePath - } - } - } - } -} diff --git a/src/main/kotlin/io/infracost/plugins/infracost/binary/InfracostBinary.kt b/src/main/kotlin/io/infracost/plugins/infracost/binary/InfracostBinary.kt new file mode 100644 index 0000000..42a0388 --- /dev/null +++ b/src/main/kotlin/io/infracost/plugins/infracost/binary/InfracostBinary.kt @@ -0,0 +1,105 @@ +package io.infracost.plugins.infracost.binary + +import com.intellij.openapi.project.Project +import io.infracost.plugins.infracost.ui.notify.InfracostNotificationGroup +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.security.MessageDigest +import java.util.zip.GZIPInputStream +import kotlin.io.path.createTempDirectory + +const val releaseUrl = "https://infracost.io/downloads/latest" + +class InfracostBinary { + companion object { + @JvmStatic + var binaryFile: String = "" + + fun downloadBinary(project: Project, targetArch: String, target: File, initial: Boolean): Boolean { + if (target.exists() && initial) { + println("Binary already exists at ${target.absolutePath}") + return false + } + + val tmpFile = kotlin.io.path.createTempFile("infracost-${targetArch}", ".tar.gz").toFile() + downloadFile("${releaseUrl}/infracost-${targetArch}.tar.gz", tmpFile) + val expectedChecksum = fetchUrl("${releaseUrl}/infracost-${targetArch}.tar.gz.sha256").split(" ")[0] + + val calculatedChecksum = calculateChecksum(tmpFile) + if (calculatedChecksum == expectedChecksum) { + unTar(tmpFile, target) + target.setExecutable(true) + binaryFile = target.absolutePath + InfracostNotificationGroup.notifyInformation(project, "Downloaded Infracost") + } else { + InfracostNotificationGroup.notifyError( + project, + "Checksum mismatch for download file, not using download" + ) + return false + } + return true + } + + private fun fetchUrl(url: String): String { + val connection = URL(url).openConnection() as HttpURLConnection + return connection.inputStream.bufferedReader().use { it.readText() } + } + + private fun downloadFile(url: String, outputFile: File) { + val connection = URL(url).openConnection() as HttpURLConnection + connection.inputStream.use { input -> + FileOutputStream(outputFile).use { output -> + input.copyTo(output) + } + } + } + + private fun calculateChecksum(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + file.inputStream().use { input -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + return digest.digest().joinToString("") { "%02x".format(it) } + } + + + private fun unTar(tarFile: File, targetFile: File) { + val outputDir = createTempDirectory().toFile() + val tarInput: InputStream = if (tarFile.extension == "gz") { + GZIPInputStream(FileInputStream(tarFile)) + } else { + FileInputStream(tarFile) + } + val tarArchiveInputStream = TarArchiveInputStream(tarInput) + + val entry: TarArchiveEntry? = tarArchiveInputStream.nextTarEntry + while (entry != null) { + val outputFile = File(outputDir, entry.name) + if (entry.isDirectory) { + outputFile.mkdirs() + } else { + outputFile.parentFile?.mkdirs() + FileOutputStream(outputFile).use { output -> + tarArchiveInputStream.copyTo(output) + } + } + Files.move(outputFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + return + } + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/infracost/plugins/infracost/listeners/InfracostFileListener.kt b/src/main/kotlin/io/infracost/plugins/infracost/listeners/InfracostFileListener.kt index 93b7c61..e56956b 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/listeners/InfracostFileListener.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/listeners/InfracostFileListener.kt @@ -17,7 +17,8 @@ class InfracostFileListener : BulkFileListener { for (event in events) { if (event.isFromSave) { if (event.file?.extension?.lowercase() in INFRACOST_FILE_EXTENSIONS || - INFRACOST_FILES.contains(event.file?.name?.lowercase())){ + INFRACOST_FILES.contains(event.file?.name?.lowercase()) + ) { val project = ProjectLocator.getInstance().guessProjectForFile(event.file!!) ?: return RunInfracostAction.runInfracost(project) } diff --git a/src/main/kotlin/io/infracost/plugins/infracost/settings/InfracostSettingsComponent.kt b/src/main/kotlin/io/infracost/plugins/infracost/settings/InfracostSettingsComponent.kt index b07cd40..3f9c662 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/settings/InfracostSettingsComponent.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/settings/InfracostSettingsComponent.kt @@ -5,9 +5,14 @@ import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.ui.components.JBLabel import com.intellij.util.ui.FormBuilder -import io.infracost.plugins.infracost.actions.tasks.InfracostTask +import io.infracost.plugins.infracost.actions.DownloadInfracostAction +import io.infracost.plugins.infracost.binary.InfracostBinary +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.SwingUtilities /** Supports creating and managing a [JPanel] for the Settings Dialog. */ class InfracostSettingsComponent { @@ -22,9 +27,20 @@ class InfracostSettingsComponent { FileChooserDescriptorFactory.createSingleFileDescriptor() ) + val button = JButton("Update Infracost") + button.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + SwingUtilities.invokeLater { + DownloadInfracostAction.runDownload(ProjectManager.getInstance().defaultProject, false) + } + } + }) + panel = FormBuilder.createFormBuilder() .addLabeledComponent(JBLabel("Specific infracost path: "), infracostPath, 1, true) + .addVerticalGap(5) + .addComponent(button) .addComponentFillVertically(JPanel(), 0) .panel } @@ -38,6 +54,6 @@ class InfracostSettingsComponent { fun setInfracostPath(newText: String) { infracostPath.text = newText - InfracostTask.resetBinaryFile() + InfracostBinary.binaryFile = newText } } diff --git a/src/main/kotlin/io/infracost/plugins/infracost/ui/InfracostWindow.kt b/src/main/kotlin/io/infracost/plugins/infracost/ui/InfracostWindow.kt index 640a76a..3afa230 100644 --- a/src/main/kotlin/io/infracost/plugins/infracost/ui/InfracostWindow.kt +++ b/src/main/kotlin/io/infracost/plugins/infracost/ui/InfracostWindow.kt @@ -11,7 +11,7 @@ import com.intellij.ui.JBColor import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane import com.intellij.ui.treeStructure.Tree -import io.infracost.plugins.infracost.actions.CheckAuthAction +import io.infracost.plugins.infracost.actions.DownloadInfracostAction import io.infracost.plugins.infracost.actions.ResultProcessor import io.infracost.plugins.infracost.actions.RunAuthAction import io.infracost.plugins.infracost.model.Resource @@ -30,8 +30,9 @@ class InfracostWindow(private val project: Project) : SimpleToolWindowPanel(fals private var authenticated: Boolean = false init { + DownloadInfracostAction.runDownload(project) configureToolbar() - CheckAuthAction.checkAuth(project) + } fun updatePanel(isAuthenticated: Boolean) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 621eef1..5a1979f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -43,5 +43,9 @@ v1.0 - Initial release
+ \ No newline at end of file