mirror: registry.coder.com/coder/jetbrains/coder v1.3.0
This commit is contained in:
184
README.md
Normal file
184
README.md
Normal file
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!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/)
|
||||
353
jetbrains.tftest.hcl
Normal file
353
jetbrains.tftest.hcl
Normal file
@@ -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."
|
||||
}
|
||||
}
|
||||
280
main.tf
Normal file
280
main.tf
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user