diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index 7fa8f6747..821882c70 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -136,6 +136,42 @@ module "jetbrains" { } ``` +### Plugin Auto‑Installer + +This module now supports automatic JetBrains plugin installation inside your workspace. + +To get a plugin ID, open the plugin’s page on the JetBrains Marketplace. Scroll down to Additional Information and look for Plugin ID. Use that value in the configuration below. + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.2.1" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + default = ["IU", "PY"] + + jetbrains_plugins = { + "PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"] + "IU" = ["", ""] + "WS" = ["", ""] + "GO" = ["", ""] + "CL" = ["", ""] + "PS" = ["", ""] + "RD" = ["", ""] + "RM" = ["", ""] + "RR" = ["", ""] + } +} +``` + +> [!IMPORTANT] +> This module prerequisites and limitations +> +> 1. Requires JetBrains Toolbox to be installed +> 2. Requires jq and zip to be available +> 3. Only works on Debian/Ubuntu-based systems (due to apt-get usage) + ### Accessing the IDE Metadata You can now reference the output `ide_metadata` as a map. diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl index dba9551da..973a74295 100644 --- a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl +++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl @@ -351,3 +351,37 @@ run "validate_output_schema" { error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test." } } + +run "no_plugin_script_when_plugins_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["PY"] + jetbrains_plugins = {} + } + + assert { + condition = length(resource.coder_script.install_jetbrains_plugins) == 0 + error_message = "Expected no plugin install script when plugins list is empty" + } +} + +run "plugin_script_created_when_plugins_provided" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["PY"] + jetbrains_plugins = { + "PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"] + } + } + + assert { + condition = length(resource.coder_script.install_jetbrains_plugins) == 1 + error_message = "Expected script to be created when plugins are provided" + } +} \ No newline at end of file diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index 2fac060f1..3b211ffd7 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -173,6 +173,13 @@ variable "ide_config" { } } +variable "jetbrains_plugins" { + type = map(list(string)) + description = "Map of IDE product codes to plugin ID lists. Example: { IU = [\"com.foo\"], GO = [\"org.bar\"] }." + default = {} +} + + locals { # Parse HTTP responses once with error handling for air-gapped environments parsed_responses = { @@ -214,6 +221,12 @@ locals { # 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) + + plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins)) + + plugin_install_script = file("${path.module}/script/install_plugins.sh") + + ide_config_b64 = base64encode(jsonencode(var.ide_config)) } data "coder_parameter" "jetbrains_ides" { @@ -241,6 +254,33 @@ data "coder_parameter" "jetbrains_ides" { data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} +resource "coder_script" "install_jetbrains_plugins" { + count = length(var.jetbrains_plugins) > 0 ? 1 : 0 + agent_id = var.agent_id + display_name = "Install JetBrains Plugins" + run_on_start = true + + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + CONFIG_DIR="$HOME/.config/jetbrains" + + mkdir -p "$CONFIG_DIR" + echo -n "${local.plugin_map_b64}" | base64 -d > "$CONFIG_DIR/plugins.json" + chmod 600 "$CONFIG_DIR/plugins.json" + + echo -n "${local.ide_config_b64}" | base64 -d > "$CONFIG_DIR/ide_config.json" + chmod 600 "$CONFIG_DIR/ide_config.json" + + echo -n '${base64encode(local.plugin_install_script)}' | base64 -d > /tmp/install_plugins.sh + chmod +x /tmp/install_plugins.sh + nohup /tmp/install_plugins.sh > /tmp/install_plugins.log 2>&1 & + exit 0 + EOT +} + resource "coder_app" "jetbrains" { for_each = local.selected_ides agent_id = var.agent_id @@ -277,4 +317,4 @@ output "ide_metadata" { # 'key' will be the IDE key (e.g., "IC", "PY") for key, val in local.selected_ides : key => local.options_metadata[key] } -} +} \ No newline at end of file diff --git a/registry/coder/modules/jetbrains/script/install_plugins.sh b/registry/coder/modules/jetbrains/script/install_plugins.sh new file mode 100644 index 000000000..0ade419f2 --- /dev/null +++ b/registry/coder/modules/jetbrains/script/install_plugins.sh @@ -0,0 +1,118 @@ +#!/bin/bash +set -euo pipefail + +LOGFILE="$HOME/.config/jetbrains/install_plugins.log" +CONFIG_DIR="$HOME/.config/jetbrains" +IDE_CONFIG="$CONFIG_DIR/ide_config.json" +PLUGIN_MAP="$CONFIG_DIR/plugins.json" +IDE_BASE="$HOME/.local/share/JetBrains" + +mkdir -p "$CONFIG_DIR" +exec > >(tee -a "$LOGFILE") 2>&1 + +log() { + printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" +} + +# ---------- Read config ---------- +get_enabled_codes() { + jq -r 'keys[]' "$PLUGIN_MAP" +} + +get_plugins_for_code() { + jq -r --arg CODE "$1" '.[$CODE][]?' "$PLUGIN_MAP" +} + +get_build_for_code() { + jq -r --arg CODE "$1" '.[$CODE].build' "$IDE_CONFIG" +} + +get_data_dir_for_code() { + local code="$1" + local build="$2" + + local name + local build_prefix + local year + local minor + + name="$(jq -r --arg CODE "$code" '.[$CODE].name' "$IDE_CONFIG")" + + # build = 253.29346.142 → prefix = 253 + build_prefix="${build%%.*}" + + # 253 → 2025.3 + year="20${build_prefix:0:2}" + minor="${build_prefix:2:1}" + + printf '%s%s.%s\n' "$name" "$year" "$minor" +} + +# ---------- Plugin installer ---------- +install_plugin() { + local code="$1" + local build="$2" + local dataDir="$3" + local pluginId="$4" + + local plugins_dir="$IDE_BASE/$dataDir" + mkdir -p "$plugins_dir" + + local url="https://plugins.jetbrains.com/pluginManager?action=download&id=$pluginId&build=$code-$build" + + local workdir + workdir="$(mktemp -d)" + cd "$workdir" + + log "Downloading $pluginId ($code-$build)" + + if ! curl -fsSL -OJ "$url"; then + log "Download failed: $pluginId" + rm -rf "$workdir" + return 1 + fi + + # We expect exactly one file after download + file="$(ls)" + + # ---------- ZIP plugin ---------- + if unzip -t "$file" > /dev/null 2>&1; then + unzip -qq "$file" + + entries=(*) + log "Extracted $file, found entries: ${entries[*]}" + + if [ -d "${entries[0]}" ] && [ -d "${entries[0]}/lib" ]; then + cp -r "${entries[0]}" "$plugins_dir/" + log "Installed ZIP plugin $pluginId" + elif [[ "$file" == *.jar ]]; then + cp "$file" "$plugins_dir/" + log "Installed JAR plugin $pluginId" + fi + fi + + cd / + rm -rf "$workdir" +} + +# ---------- Main ---------- +log "Plugin installer started (build-based mode)" + +[ ! -f "$PLUGIN_MAP" ] && log "No plugins.json found" && exit 0 +[ ! -f "$IDE_CONFIG" ] && log "No ide_config.json found" && exit 0 + +get_enabled_codes | while read -r code; do + build="$(get_build_for_code "$code")" + dataDir="$(get_data_dir_for_code "$code" "$build")" + + if [ -z "$build" ] || [ -z "$dataDir" ]; then + log "Missing config for $code, skipping" + continue + fi + + get_plugins_for_code "$code" | while read -r plugin; do + [ -n "$plugin" ] && install_plugin "$code" "$build" "$dataDir" "$plugin" || continue + done +done + +log "All plugins processed"