From 901d120c04ae5cdd8d68bac61c147eadfe64796d Mon Sep 17 00:00:00 2001 From: Coder Module Mirror Date: Fri, 13 Mar 2026 15:50:18 +0100 Subject: [PATCH] mirror: registry.coder.com/coder/jetbrains/coder v1.3.0 --- README.md | 184 ++++++++++++++++++++++ jetbrains.tftest.hcl | 353 +++++++++++++++++++++++++++++++++++++++++++ main.tf | 280 ++++++++++++++++++++++++++++++++++ 3 files changed, 817 insertions(+) create mode 100644 README.md create mode 100644 jetbrains.tftest.hcl create mode 100644 main.tf diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fa8f67 --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +--- +display_name: JetBrains Toolbox +description: Add JetBrains IDE integrations to your Coder workspaces with configurable options. +icon: ../../../../.icons/jetbrains.svg +verified: true +tags: [ide, jetbrains, parameter] +--- + +# JetBrains IDEs + +This module adds JetBrains IDE buttons to launch IDEs directly from the dashboard by integrating with the JetBrains Toolbox. + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" +} +``` + +![JetBrains IDEs list](../../.images/jetbrains-dropdown.png) + +> [!IMPORTANT] +> This module requires Coder version 2.24+ and [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) version 2.7 or higher. + +> [!WARNING] +> JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM. +> Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements. + +## Examples + +### Pre-configured Mode (Direct App Creation) + +When `default` contains IDE codes, those IDEs are created directly without user selection: + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA +} +``` + +### User Choice with Limited Options + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + # Show parameter with limited options + options = ["IU", "PY"] # Only these IDEs are available for selection +} +``` + +### Early Access Preview (EAP) Versions + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + default = ["IU", "PY"] + channel = "eap" # Use Early Access Preview versions + major_version = "2025.2" # Specific major version +} +``` + +### Custom IDE Configuration + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/workspace/project" + + # Custom IDE metadata (display names and icons) + ide_config = { + "IU" = { + name = "IntelliJ IDEA" + icon = "/custom/icons/intellij.svg" + build = "251.26927.53" + } + + "PY" = { + name = "PyCharm" + icon = "/custom/icons/pycharm.svg" + build = "251.23774.211" + } + } +} +``` + +### Single IDE for Specific Use Case + +```tf +module "jetbrains_pycharm" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/workspace/project" + + default = ["PY"] # Only PyCharm + + # Specific version for consistency + major_version = "2025.1" + channel = "release" +} +``` + +### Custom Tooltip + +Add helpful tooltip text that appears when users hover over the IDE app buttons: + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + default = ["IU", "PY"] + tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button." +} +``` + +### Accessing the IDE Metadata + +You can now reference the output `ide_metadata` as a map. + +```tf +# Add metadata to the container showing the installed IDEs and their build versions. +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = one(docker_container.workspace).id + + dynamic "item" { + for_each = length(module.jetbrains) > 0 ? one(module.jetbrains).ide_metadata : {} + content { + key = item.value.build + value = "${item.value.name} [${item.key}]" + } + } +} +``` + +## Behavior + +### Parameter vs Direct Apps + +- **`default = []` (empty)**: Creates a `coder_parameter` allowing users to select IDEs from `options` +- **`default` with values**: Skips parameter and directly creates `coder_app` resources for the specified IDEs + +### Version Resolution + +- Build numbers are fetched from the JetBrains API for the latest compatible versions when internet access is available +- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config` +- `major_version` and `channel` control which API endpoint is queried (when API access is available) + +## Supported IDEs + +All JetBrains IDEs with remote development capabilities: + +- [CLion (`CL`)](https://www.jetbrains.com/clion/) +- [GoLand (`GO`)](https://www.jetbrains.com/go/) +- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/) +- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/) +- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/) +- [Rider (`RD`)](https://www.jetbrains.com/rider/) +- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/) +- [RustRover (`RR`)](https://www.jetbrains.com/rust/) +- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/) diff --git a/jetbrains.tftest.hcl b/jetbrains.tftest.hcl new file mode 100644 index 0000000..dba9551 --- /dev/null +++ b/jetbrains.tftest.hcl @@ -0,0 +1,353 @@ +variables { + # Default IDE config, mirrored from main.tf for test assertions. + # If main.tf defaults change, update this map to match. + expected_ide_config = { + "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" }, + "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" }, + "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" }, + "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" }, + "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" }, + "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" }, + "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" }, + "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" }, + "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" } + } +} + +run "validate_test_config_matches_defaults" { + command = plan + + variables { + # Provide minimal vars to allow plan to read module variables + agent_id = "foo" + folder = "/home/coder" + } + + assert { + condition = length(var.ide_config) == length(var.expected_ide_config) + error_message = "Test configuration mismatch: 'var.ide_config' in main.tf has ${length(var.ide_config)} items, but 'var.expected_ide_config' in the test file has ${length(var.expected_ide_config)} items. Please update the test file's global variables block." + } + + assert { + # Check that all keys in the test local are present in the module's default + condition = alltrue([ + for key in keys(var.expected_ide_config) : + can(var.ide_config[key]) + ]) + error_message = "Test configuration mismatch: Keys in 'var.expected_ide_config' are out of sync with 'var.ide_config' defaults. Please update the test file's global variables block." + } + + assert { + # Check if all build numbers in the test local match the module's defaults + # This relies on the previous two assertions passing (same length, same keys) + condition = alltrue([ + for key, config in var.expected_ide_config : + var.ide_config[key].build == config.build + ]) + error_message = "Test configuration mismatch: One or more build numbers in 'var.expected_ide_config' do not match the defaults in 'var.ide_config'. Please update the test file's global variables block." + } +} + +run "requires_agent_and_folder" { + command = plan + + # Setting both required vars should plan + variables { + agent_id = "foo" + folder = "/home/coder" + } +} + +run "creates_parameter_when_default_empty_latest" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + major_version = "latest" + } + + # When default is empty, a coder_parameter should be created + assert { + condition = can(data.coder_parameter.jetbrains_ides[0].type) + error_message = "Expected data.coder_parameter.jetbrains_ides to exist when default is empty" + } +} + +run "no_apps_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + } + + assert { + condition = length(resource.coder_app.jetbrains) == 0 + error_message = "Expected no coder_app resources when default is empty" + } +} + +run "single_app_when_default_GO" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + } + + assert { + condition = length(resource.coder_app.jetbrains) == 1 + error_message = "Expected exactly one coder_app when default contains GO" + } +} + +run "url_contains_required_params" { + command = apply + + variables { + agent_id = "test-agent-123" + folder = "/custom/project/path" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("jetbrains://gateway/coder", app.url)) > 0]) + error_message = "URL must contain jetbrains scheme" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&folder=/custom/project/path", app.url)) > 0]) + error_message = "URL must include folder path" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_product_code=GO", app.url)) > 0]) + error_message = "URL must include product code" + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_build_number=", app.url)) > 0]) + error_message = "URL must include build number" + } +} + +run "includes_agent_name_when_set" { + command = apply + + variables { + agent_id = "test-agent-123" + agent_name = "main-agent" + folder = "/custom/project/path" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&agent_name=main-agent", app.url)) > 0]) + error_message = "URL must include agent_name when provided" + } +} + +run "parameter_order_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + coder_parameter_order = 5 + } + + assert { + condition = data.coder_parameter.jetbrains_ides[0].order == 5 + error_message = "Expected coder_parameter order to be set to 5" + } +} + +run "app_order_when_default_not_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + coder_app_order = 10 + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.order == 10]) + error_message = "Expected coder_app order to be set to 10" + } +} + +run "tooltip_when_provided" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button." + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."]) + error_message = "Expected coder_app tooltip to be set when provided" + } +} + +run "tooltip_default_when_not_provided" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + } + + assert { + condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."]) + error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided" + } +} + +run "channel_eap" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + channel = "eap" + major_version = "latest" + } + + assert { + condition = output.ide_metadata["GO"].json_data.type == "eap" + error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'" + } +} + +run "specific_major_version" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + major_version = "2025.3" + } + + assert { + condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3" + error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'" + } +} + +run "output_empty_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + # var.default is empty + } + + assert { + condition = length(output.ide_metadata) == 0 + error_message = "Expected ide_metadata output to be empty when var.default is not set" + } +} + +run "output_single_ide_uses_fallback_build" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + # Force HTTP data source to fail to test fallback logic + releases_base_link = "https://coder.com" + } + + assert { + condition = length(output.ide_metadata) == 1 + error_message = "Expected ide_metadata output to have 1 item" + } + + assert { + condition = can(output.ide_metadata["GO"]) + error_message = "Expected ide_metadata output to have key 'GO'" + } + + assert { + condition = output.ide_metadata["GO"].name == var.expected_ide_config["GO"].name + error_message = "Expected ide_metadata['GO'].name to be '${var.expected_ide_config["GO"].name}'" + } + + assert { + condition = output.ide_metadata["GO"].build == var.expected_ide_config["GO"].build + error_message = "Expected ide_metadata['GO'].build to use the fallback '${var.expected_ide_config["GO"].build}'" + } + + assert { + condition = output.ide_metadata["GO"].icon == var.expected_ide_config["GO"].icon + error_message = "Expected ide_metadata['GO'].icon to be '${var.expected_ide_config["GO"].icon}'" + } +} + +run "output_multiple_ides" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["IU", "PY"] + # Force HTTP data source to fail to test fallback logic + releases_base_link = "https://coder.com" + } + + assert { + condition = length(output.ide_metadata) == 2 + error_message = "Expected ide_metadata output to have 2 items" + } + + assert { + condition = can(output.ide_metadata["IU"]) && can(output.ide_metadata["PY"]) + error_message = "Expected ide_metadata output to have keys 'IU' and 'PY'" + } + + assert { + condition = output.ide_metadata["PY"].name == var.expected_ide_config["PY"].name + error_message = "Expected ide_metadata['PY'].name to be '${var.expected_ide_config["PY"].name}'" + } + + assert { + condition = output.ide_metadata["PY"].build == var.expected_ide_config["PY"].build + error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'" + } +} +run "validate_output_schema" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + } + + assert { + condition = alltrue([ + for key, meta in output.ide_metadata : ( + can(meta.icon) && + can(meta.name) && + can(meta.identifier) && + can(meta.key) && + can(meta.build) && + # json_data can be null, but the key must exist + can(meta.json_data) + ) + ]) + error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test." + } +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..2fac060 --- /dev/null +++ b/main.tf @@ -0,0 +1,280 @@ +terraform { + required_version = ">= 1.9" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + http = { + source = "hashicorp/http" + version = ">= 3.0" + } + } +} + +variable "agent_id" { + type = string + description = "The resource ID of a Coder agent." +} + +variable "agent_name" { + type = string + description = "The name of a Coder agent. Needed for workspaces with multiple agents." + default = null +} + +variable "folder" { + type = string + description = "The directory to open in the IDE. e.g. /home/coder/project" + validation { + condition = can(regex("^(?:/[^/]+)+/?$", var.folder)) + error_message = "The folder must be a full path and must not start with a ~." + } +} + +variable "default" { + default = [] + type = set(string) + description = <<-EOT + The default IDE selection. Removes the selection from the UI. e.g. ["CL", "GO", "IU"] + EOT +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "coder_app_order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + +variable "tooltip" { + type = string + description = "Markdown text that is displayed when hovering over workspace apps." + default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button." +} + +variable "major_version" { + type = string + description = "The major version of the IDE. i.e. 2025.1" + default = "latest" + validation { + condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest" + error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'" + } +} + +variable "channel" { + type = string + description = "JetBrains IDE release channel. Valid values are release and eap." + default = "release" + validation { + condition = can(regex("^(release|eap)$", var.channel)) + error_message = "The channel must be either release or eap." + } +} + +variable "options" { + type = set(string) + description = "The list of IDE product codes." + default = ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"] + validation { + condition = ( + alltrue([ + for code in var.options : contains(["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code) + ]) + ) + error_message = "The options must be a set of valid product codes. Valid product codes are ${join(",", ["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"])}." + } + # check if the set is empty + validation { + condition = length(var.options) > 0 + error_message = "The options must not be empty." + } +} + +variable "releases_base_link" { + type = string + description = "URL of the JetBrains releases base link." + default = "https://data.services.jetbrains.com" + validation { + condition = can(regex("^https?://.+$", var.releases_base_link)) + error_message = "The releases_base_link must be a valid HTTP/S address." + } +} + +variable "download_base_link" { + type = string + description = "URL of the JetBrains download base link." + default = "https://download.jetbrains.com" + validation { + condition = can(regex("^https?://.+$", var.download_base_link)) + error_message = "The download_base_link must be a valid HTTP/S address." + } +} + +data "http" "jetbrains_ide_versions" { + for_each = length(var.default) == 0 ? var.options : var.default + url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}" +} + +variable "ide_config" { + description = <<-EOT + A map of JetBrains IDE configurations. + The key is the product code and the value is an object with the following properties: + - name: The name of the IDE. + - icon: The icon of the IDE. + - build: The build number of the IDE. + Example: + { + "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" }, + "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" }, + "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" }, + } + EOT + type = map(object({ + name = string + icon = string + build = string + })) + default = { + "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" }, + "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" }, + "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" }, + "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" }, + "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" }, + "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" }, + "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" }, + "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" }, + "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" } + } + validation { + condition = length(var.ide_config) > 0 + error_message = "The ide_config must not be empty." + } + # ide_config must be a superset of var.options + # Requires Terraform 1.9+ for cross-variable validation references + validation { + condition = alltrue([ + for code in var.options : contains(keys(var.ide_config), code) + ]) + error_message = "The ide_config must be a superset of var.options." + } +} + +locals { + # Parse HTTP responses once with error handling for air-gapped environments + parsed_responses = { + for code in length(var.default) == 0 ? var.options : var.default : code => try( + jsondecode(data.http.jetbrains_ide_versions[code].response_body), + {} # Return empty object if API call fails + ) + } + + # Filter the parsed response for the requested major version if not "latest" + filtered_releases = { + for code in length(var.default) == 0 ? var.options : var.default : code => [ + for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) : + r if var.major_version == "latest" || r.majorVersion == var.major_version + ] + } + + # Select the latest release for the requested major version (first item in the filtered list) + selected_releases = { + for code in length(var.default) == 0 ? var.options : var.default : code => + length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null + } + + # Dynamically generate IDE configurations based on options with fallback to ide_config + options_metadata = { + for code in length(var.default) == 0 ? var.options : var.default : code => { + icon = var.ide_config[code].icon + name = var.ide_config[code].name + identifier = code + key = code + + # Use API build number if available, otherwise fall back to ide_config build number + build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build + + # Store API data for potential future use + json_data = local.selected_releases[code] + } + } + + # Convert the parameter value to a set for for_each + selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default) +} + +data "coder_parameter" "jetbrains_ides" { + count = length(var.default) == 0 ? 1 : 0 + type = "list(string)" + name = "jetbrains_ides" + description = "Select which JetBrains IDEs to configure for use in this workspace." + display_name = "JetBrains IDEs" + icon = "/icon/jetbrains-toolbox.svg" + mutable = true + default = jsonencode([]) + order = var.coder_parameter_order + form_type = "multi-select" # requires Coder version 2.24+ + + dynamic "option" { + for_each = var.options + content { + icon = var.ide_config[option.value].icon + name = var.ide_config[option.value].name + value = option.value + } + } +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_app" "jetbrains" { + for_each = local.selected_ides + agent_id = var.agent_id + slug = "jetbrains-${lower(each.key)}" + display_name = local.options_metadata[each.key].name + icon = local.options_metadata[each.key].icon + external = true + order = var.coder_app_order + group = var.group + tooltip = var.tooltip + url = join("", [ + "jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox + data.coder_workspace.me.name, + "&owner=", + data.coder_workspace_owner.me.name, + "&folder=", + var.folder, + "&url=", + data.coder_workspace.me.access_url, + "&token=", + "$SESSION_TOKEN", + "&ide_product_code=", + each.key, + "&ide_build_number=", + local.options_metadata[each.key].build, + var.agent_name != null ? "&agent_name=${var.agent_name}" : "", + ]) +} + +output "ide_metadata" { + description = "A map of the metadata for each selected JetBrains IDE." + value = { + # We iterate directly over the selected_ides map. + # 'key' will be the IDE key (e.g., "IC", "PY") + for key, val in local.selected_ides : key => local.options_metadata[key] + } +}