fix(jetbrains): scope HTTP version fetch to selected IDEs only (#822)

## Problem

The `data "http" "jetbrains_ide_versions"` resource fetches release info
from `data.services.jetbrains.com` for **all configured IDE options** at
plan time, regardless of what the user actually selected. When the API
is unreachable (air-gapped environments, DNS failures, transient
outages), this causes a fatal Terraform error that blocks the workspace
build — even when no JetBrains IDEs were selected.

## Fix

Changed the `for_each` on the HTTP data source (and all dependent
locals) from iterating over `var.options`/`var.default` to
`local.selected_ides` — the user's actual selection.

| Scenario | Before | After |
|---|---|---|
| No IDEs selected (`[]`) | 9 HTTP requests | 0 HTTP requests |
| 1 IDE selected (`["GO"]`) | 9 HTTP requests | 1 HTTP request |
| All IDEs selected | 9 HTTP requests | 9 HTTP requests |

## Validation

- All 17 existing `terraform test` cases pass
- Tested end-to-end on [dev.coder.com](https://dev.coder.com) with
Docker template:
  - `jetbrains_ides=[]` — zero HTTP requests, build succeeds
- `jetbrains_ides=["GO"]` — single HTTP request for GoLand only,
`coder_app.jetbrains["GO"]` created

Closes #821

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
This commit is contained in:
Atif Ali 2026-04-01 15:33:03 +00:00 committed by GitHub
parent 19f6dc947f
commit fc66478b94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 18 additions and 16 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.0" 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.0" 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.0" 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.0" 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"]
@ -81,7 +81,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.0" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/workspace/project" folder = "/workspace/project"
@ -108,7 +108,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.0" version = "1.4.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/workspace/project" folder = "/workspace/project"
@ -128,7 +128,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.0" 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"]

View File

@ -125,7 +125,7 @@ variable "download_base_link" {
} }
data "http" "jetbrains_ide_versions" { data "http" "jetbrains_ide_versions" {
for_each = length(var.default) == 0 ? var.options : var.default for_each = local.selected_ides
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" : ""}"
} }
@ -174,9 +174,14 @@ variable "ide_config" {
} }
locals { locals {
# Determine the user's actual IDE selection.
# This is computed before the HTTP data source so that version lookups
# 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)
# Parse HTTP responses once with error handling for air-gapped environments # Parse HTTP responses once with error handling for air-gapped environments
parsed_responses = { parsed_responses = {
for code in length(var.default) == 0 ? var.options : var.default : code => try( for code in local.selected_ides : code => try(
jsondecode(data.http.jetbrains_ide_versions[code].response_body), jsondecode(data.http.jetbrains_ide_versions[code].response_body),
{} # Return empty object if API call fails {} # Return empty object if API call fails
) )
@ -184,7 +189,7 @@ locals {
# 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 length(var.default) == 0 ? var.options : var.default : code => [ for code in local.selected_ides : code => [
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) : 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 r if var.major_version == "latest" || r.majorVersion == var.major_version
] ]
@ -192,13 +197,13 @@ locals {
# 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 length(var.default) == 0 ? var.options : var.default : code => for code in local.selected_ides : code =>
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
} }
# Dynamically generate IDE configurations based on options with fallback to ide_config # Dynamically generate IDE configurations based on selected IDEs with fallback to ide_config
options_metadata = { options_metadata = {
for code in length(var.default) == 0 ? var.options : var.default : code => { for code in local.selected_ides : code => {
icon = var.ide_config[code].icon icon = var.ide_config[code].icon
name = var.ide_config[code].name name = var.ide_config[code].name
identifier = code identifier = code
@ -211,9 +216,6 @@ locals {
json_data = local.selected_releases[code] 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" { data "coder_parameter" "jetbrains_ides" {