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] } }