feat(jetbrains): skip HTTP calls when ide_config is set (#836)

Fixes #835

## Problem

The `data "http"` resource always fires for every selected IDE, even
when the user has pinned versions via `ide_config`. In air-gapped or
caching scenarios, this causes:

- **30-second hangs** when `releases_base_link` is set to a dummy URL
like `https://localhost`
- **Fatal errors** with `https://localhost:1` (connection refused)
- The documented "air-gapped fallback" via `try()` never actually worked
— the `http` data source fails before `try()` can catch anything

## Fix

When `ide_config` is provided, the module now skips all HTTP calls and
uses the pinned build numbers directly.

| Scenario | `ide_config` | HTTP calls | Build source | On API failure |
|---|---|---|---|---|
| User wants latest | `null` (default) | Yes | JetBrains API | Terraform
error (fail loudly) |
| User pins versions | Set | **None** | `ide_config.build` | N/A |

### Changes

- `ide_config` default changed from a full map to `null`
- `name` and `icon` are now `optional(string)` in `ide_config` — falls
back to built-in metadata
- `data.http.jetbrains_ide_versions` `for_each` is empty when
`ide_config` is set
- Static `ide_metadata` local provides name/icon when `ide_config` is
null
- Removed `try()` fallback from `parsed_responses` — API errors are now
explicit instead of silently using stale builds
- Cross-variable validation rejects `major_version`, `channel`, and
`releases_base_link` when `ide_config` is set
- Validation for `ide_config ⊇ default` added (previously only
`ide_config ⊇ options` was checked)
- Version bumped `1.3.1` → `1.4.0`

### Usage

```tf
module "jetbrains" {
  source   = "registry.coder.com/coder/jetbrains/coder"
  version  = "1.4.0"
  agent_id = coder_agent.main.id
  folder   = "/home/coder/project"

  # Zero HTTP calls — only build is required.
  ide_config = {
    "GO" = { build = "261.22158.291" }
    "PY" = { build = "261.22158.340" }
  }
  options = ["GO", "PY"]
}
```

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑‍💻
This commit is contained in:
Atif Ali 2026-04-09 07:28:57 +00:00 committed by GitHub
parent d3885a5047
commit b76b544e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 274 additions and 133 deletions

View File

@ -14,7 +14,7 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -39,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA
@ -52,7 +52,7 @@ module "jetbrains" {
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
# Show parameter with limited options # Show parameter with limited options
@ -66,7 +66,7 @@ module "jetbrains" {
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
default = ["IU", "PY"] default = ["IU", "PY"]
@ -75,30 +75,37 @@ module "jetbrains" {
} }
``` ```
### Custom IDE Configuration ### Pinned Versions (Air-Gapped / Cached)
When `ide_config` is set, the module makes zero HTTP calls and uses the
provided build numbers directly. This is ideal for air-gapped environments
or when caching IDE installations.
> [!TIP]
> To find the latest build number for an IDE, query the JetBrains releases API:
>
> ```sh
> curl -s "https://data.services.jetbrains.com/products/releases?code=GO&type=release&latest=true" | jq 'to_entries[0].value[0] | {build, version}'
> ```
>
> Replace `GO` with the product code for the IDE you want (e.g. `IU`, `PY`, `CL`).
```tf ```tf
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/workspace/project" folder = "/home/coder/project"
# Custom IDE metadata (display names and icons) # Only build is required. Name and icon fall back to built-in defaults.
ide_config = { ide_config = {
"IU" = { "GO" = { build = "261.22158.291" }
name = "IntelliJ IDEA" "PY" = { build = "261.22158.340" }
icon = "/custom/icons/intellij.svg" # Add entries for other IDEs as needed.
build = "251.26927.53"
} }
"PY" = { options = ["GO", "PY"] # Must match the keys in ide_config.
name = "PyCharm"
icon = "/custom/icons/pycharm.svg"
build = "251.23774.211"
}
}
} }
``` ```
@ -108,7 +115,7 @@ module "jetbrains" {
module "jetbrains_pycharm" { module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/workspace/project" folder = "/workspace/project"
@ -128,7 +135,7 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.1" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
default = ["IU", "PY"] default = ["IU", "PY"]
@ -165,9 +172,9 @@ resource "coder_metadata" "container_info" {
### Version Resolution ### Version Resolution
- Build numbers are fetched from the JetBrains API for the latest compatible versions when internet access is available - **`ide_config` not set (default)**: Build numbers are fetched from the JetBrains releases API. If the API is unreachable, Terraform will return an error rather than silently using stale versions.
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config` - **`ide_config` set**: The module skips all HTTP calls and uses the provided build numbers directly. No network access required. Ideal for air-gapped deployments or when caching IDE installations.
- `major_version` and `channel` control which API endpoint is queried (when API access is available) - `major_version` and `channel` control which API endpoint is queried (only when `ide_config` is not set).
## Supported IDEs ## Supported IDEs

View File

@ -1,53 +1,3 @@
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" { run "requires_agent_and_folder" {
command = plan command = plan
@ -259,15 +209,17 @@ run "output_empty_when_default_empty" {
} }
} }
run "output_single_ide_uses_fallback_build" { run "uses_ide_config_when_set" {
command = plan command = plan
variables { variables {
agent_id = "foo" agent_id = "foo"
folder = "/home/coder" folder = "/home/coder"
default = ["GO"] default = ["GO"]
# Force HTTP data source to fail to test fallback logic options = ["GO"]
releases_base_link = "https://coder.com" ide_config = {
"GO" = { name = "GoLand Custom", icon = "/icon/goland.svg", build = "999.99999.999" }
}
} }
assert { assert {
@ -281,30 +233,38 @@ run "output_single_ide_uses_fallback_build" {
} }
assert { assert {
condition = output.ide_metadata["GO"].name == var.expected_ide_config["GO"].name condition = output.ide_metadata["GO"].name == "GoLand Custom"
error_message = "Expected ide_metadata['GO'].name to be '${var.expected_ide_config["GO"].name}'" error_message = "Expected ide_metadata['GO'].name to be 'GoLand Custom'"
} }
assert { assert {
condition = output.ide_metadata["GO"].build == var.expected_ide_config["GO"].build condition = output.ide_metadata["GO"].build == "999.99999.999"
error_message = "Expected ide_metadata['GO'].build to use the fallback '${var.expected_ide_config["GO"].build}'" error_message = "Expected ide_metadata['GO'].build to use the pinned build '999.99999.999'"
} }
assert { assert {
condition = output.ide_metadata["GO"].icon == var.expected_ide_config["GO"].icon condition = output.ide_metadata["GO"].icon == "/icon/goland.svg"
error_message = "Expected ide_metadata['GO'].icon to be '${var.expected_ide_config["GO"].icon}'" error_message = "Expected ide_metadata['GO'].icon to be '/icon/goland.svg'"
}
assert {
condition = output.ide_metadata["GO"].json_data == null
error_message = "Expected ide_metadata['GO'].json_data to be null when using ide_config"
} }
} }
run "output_multiple_ides" { run "uses_ide_config_for_multiple_ides" {
command = plan command = plan
variables { variables {
agent_id = "foo" agent_id = "foo"
folder = "/home/coder" folder = "/home/coder"
default = ["IU", "PY"] default = ["IU", "PY"]
# Force HTTP data source to fail to test fallback logic options = ["IU", "PY"]
releases_base_link = "https://coder.com" ide_config = {
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "111.11111.111" }
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "222.22222.222" }
}
} }
assert { assert {
@ -318,15 +278,50 @@ run "output_multiple_ides" {
} }
assert { assert {
condition = output.ide_metadata["PY"].name == var.expected_ide_config["PY"].name condition = output.ide_metadata["PY"].name == "PyCharm"
error_message = "Expected ide_metadata['PY'].name to be '${var.expected_ide_config["PY"].name}'" error_message = "Expected ide_metadata['PY'].name to be 'PyCharm'"
} }
assert { assert {
condition = output.ide_metadata["PY"].build == var.expected_ide_config["PY"].build condition = output.ide_metadata["PY"].build == "222.22222.222"
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'" error_message = "Expected ide_metadata['PY'].build to be the pinned build '222.22222.222'"
}
assert {
condition = output.ide_metadata["IU"].build == "111.11111.111"
error_message = "Expected ide_metadata['IU'].build to be the pinned build '111.11111.111'"
}
assert {
condition = output.ide_metadata["IU"].json_data == null
error_message = "Expected ide_metadata['IU'].json_data to be null when using ide_config"
}
assert {
condition = output.ide_metadata["PY"].json_data == null
error_message = "Expected ide_metadata['PY'].json_data to be null when using ide_config"
} }
} }
run "ide_config_build_in_url" {
command = apply
variables {
agent_id = "test-agent-123"
folder = "/home/coder/project"
default = ["GO"]
options = ["GO"]
ide_config = {
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "999.99999.999" }
}
}
assert {
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_build_number=999.99999.999", app.url)) > 0])
error_message = "URL must include the pinned build number from ide_config"
}
}
run "validate_output_schema" { run "validate_output_schema" {
command = plan command = plan
@ -334,6 +329,10 @@ run "validate_output_schema" {
agent_id = "foo" agent_id = "foo"
folder = "/home/coder" folder = "/home/coder"
default = ["GO"] default = ["GO"]
options = ["GO"]
ide_config = {
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" }
}
} }
assert { assert {
@ -351,3 +350,107 @@ run "validate_output_schema" {
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test." error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
} }
} }
run "rejects_major_version_with_ide_config" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
options = ["GO"]
major_version = "2025.3"
ide_config = {
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.31033.129" }
}
}
expect_failures = [
var.ide_config,
]
}
run "rejects_default_not_in_ide_config" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO", "IU"]
options = ["GO", "IU"]
ide_config = {
"GO" = { build = "253.31033.129" }
}
}
expect_failures = [
var.ide_config,
]
}
run "ide_config_with_build_only" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
options = ["GO"]
ide_config = {
"GO" = { build = "999.99999.999" }
}
}
assert {
condition = output.ide_metadata["GO"].name == "GoLand"
error_message = "Expected name to fall back to ide_metadata when not set in ide_config"
}
assert {
condition = output.ide_metadata["GO"].icon == "/icon/goland.svg"
error_message = "Expected icon to fall back to ide_metadata when not set in ide_config"
}
assert {
condition = output.ide_metadata["GO"].build == "999.99999.999"
error_message = "Expected build to use ide_config value"
}
}
run "rejects_releases_base_link_with_ide_config" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
options = ["GO"]
releases_base_link = "https://internal.mirror.example.com"
ide_config = {
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.31033.129" }
}
}
expect_failures = [
var.ide_config,
]
}
run "rejects_channel_with_ide_config" {
command = plan
variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
options = ["GO"]
channel = "eap"
ide_config = {
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.31033.129" }
}
}
expect_failures = [
var.ide_config,
]
}

View File

@ -125,95 +125,126 @@ variable "download_base_link" {
} }
data "http" "jetbrains_ide_versions" { data "http" "jetbrains_ide_versions" {
for_each = local.selected_ides for_each = var.ide_config == null ? local.selected_ides : toset([])
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}" url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
} }
variable "ide_config" { variable "ide_config" {
description = <<-EOT description = <<-EOT
A map of JetBrains IDE configurations. Optional map of JetBrains IDE configurations keyed by product code.
The key is the product code and the value is an object with the following properties: When null (default), the module fetches the latest build numbers from
- name: The name of the IDE. the JetBrains API at plan time. When set, all HTTP calls are skipped
- icon: The icon of the IDE. and the provided build numbers are used directly useful for
- build: The build number of the IDE. air-gapped environments or pinning specific versions.
Each value must contain:
- build: Full build number (e.g. "253.28294.337").
Optionally override the default display name or icon:
- name: Display name of the IDE (e.g. "GoLand").
- icon: Path or URL to the IDE icon (e.g. "/icon/goland.svg").
Example: Example:
{ {
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" }, "GO" = { build = "261.22158.291" },
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" }, "IU" = { build = "261.22158.277" },
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
} }
EOT EOT
type = map(object({ type = map(object({
name = string
icon = string
build = string build = string
name = optional(string)
icon = optional(string)
})) }))
default = { default = null
"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 { validation {
condition = length(var.ide_config) > 0 condition = var.ide_config == null || length(var.ide_config) > 0
error_message = "The ide_config must not be empty." error_message = "The ide_config must not be empty."
} }
# ide_config must be a superset of var.options # ide_config must be a superset of var.options
# Requires Terraform 1.9+ for cross-variable validation references # Requires Terraform 1.9+ for cross-variable validation references
validation { validation {
condition = alltrue([ condition = var.ide_config == null || alltrue([
for code in var.options : contains(keys(var.ide_config), code) for code in var.options : contains(keys(var.ide_config), code)
]) ])
error_message = "The ide_config must be a superset of var.options." error_message = "The ide_config must contain entries for all IDE codes in var.options. Either add the missing entries to ide_config or narrow var.options to match."
}
# ide_config must also cover all codes in var.default to avoid
# key-not-found errors when building options_metadata.
validation {
condition = var.ide_config == null || alltrue([
for code in var.default : contains(keys(var.ide_config), code)
])
error_message = "The ide_config must contain entries for all IDE codes in var.default."
}
# major_version, channel, and releases_base_link only affect the
# HTTP call, which is skipped when ide_config is set. Reject
# non-default values to avoid silently ignoring user intent.
validation {
condition = var.ide_config == null || (
var.major_version == "latest" &&
var.channel == "release" &&
var.releases_base_link == "https://data.services.jetbrains.com"
)
error_message = "major_version, channel, and releases_base_link have no effect when ide_config is set. Remove them or unset ide_config."
} }
} }
locals { locals {
# Static IDE metadata for name and icon lookups when ide_config is null.
ide_metadata = {
"CL" = { name = "CLion", icon = "/icon/clion.svg" }
"GO" = { name = "GoLand", icon = "/icon/goland.svg" }
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg" }
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg" }
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg" }
"RD" = { name = "Rider", icon = "/icon/rider.svg" }
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg" }
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg" }
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg" }
}
# Determine the user's actual IDE selection. # Determine the user's actual IDE selection.
# This is computed before the HTTP data source so that version lookups # This is computed before the HTTP data source so that version lookups
# are only performed for IDEs the user chose not every option. # are only performed for IDEs the user chose not every option.
selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default) selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default)
# Parse HTTP responses once with error handling for air-gapped environments # Parse HTTP responses. Only populated when ide_config is null
# and the module fetches versions from the JetBrains API.
# No try() fallback if the API is expected and fails, Terraform
# should error rather than silently using stale build numbers.
parsed_responses = { parsed_responses = {
for code in local.selected_ides : code => try( for code, response in data.http.jetbrains_ide_versions :
jsondecode(data.http.jetbrains_ide_versions[code].response_body), code => jsondecode(response.response_body)
{} # Return empty object if API call fails
)
} }
# Filter the parsed response for the requested major version if not "latest" # Filter the parsed response for the requested major version if not "latest"
filtered_releases = { filtered_releases = {
for code in local.selected_ides : code => [ for code, parsed in local.parsed_responses : code => [
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) : for r in parsed[keys(parsed)[0]] :
r if var.major_version == "latest" || r.majorVersion == var.major_version 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) # Select the latest release for the requested major version (first item in the filtered list)
selected_releases = { selected_releases = {
for code in local.selected_ides : code => for code, releases in local.filtered_releases :
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null code => length(releases) > 0 ? releases[0] : null
} }
# Dynamically generate IDE configurations based on selected IDEs with fallback to ide_config # Dynamically generate IDE configurations based on selected IDEs
options_metadata = { options_metadata = {
for code in local.selected_ides : code => { for code in local.selected_ides : code => {
icon = var.ide_config[code].icon icon = var.ide_config != null ? coalesce(var.ide_config[code].icon, local.ide_metadata[code].icon) : local.ide_metadata[code].icon
name = var.ide_config[code].name name = var.ide_config != null ? coalesce(var.ide_config[code].name, local.ide_metadata[code].name) : local.ide_metadata[code].name
identifier = code identifier = code
key = code key = code
# Use API build number if available, otherwise fall back to ide_config build number # When ide_config is set, use the pinned build number directly.
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build # When fetching from API, use the API result (fails if unavailable).
build = var.ide_config != null ? var.ide_config[code].build : local.selected_releases[code].build
# Store API data for potential future use # API response data, null when using ide_config.
json_data = local.selected_releases[code] json_data = var.ide_config != null ? null : local.selected_releases[code]
} }
} }
} }
@ -233,8 +264,8 @@ data "coder_parameter" "jetbrains_ides" {
dynamic "option" { dynamic "option" {
for_each = var.options for_each = var.options
content { content {
icon = var.ide_config[option.value].icon icon = var.ide_config != null ? coalesce(var.ide_config[option.value].icon, local.ide_metadata[option.value].icon) : local.ide_metadata[option.value].icon
name = var.ide_config[option.value].name name = var.ide_config != null ? coalesce(var.ide_config[option.value].name, local.ide_metadata[option.value].name) : local.ide_metadata[option.value].name
value = option.value value = option.value
} }
} }