Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" = ["<Plugin-ID>", "<Plugin-ID>"]
"WS" = ["<Plugin-ID>", "<Plugin-ID>"]
"GO" = ["<Plugin-ID>", "<Plugin-ID>"]
"CL" = ["<Plugin-ID>", "<Plugin-ID>"]
"PS" = ["<Plugin-ID>", "<Plugin-ID>"]
"RD" = ["<Plugin-ID>", "<Plugin-ID>"]
"RM" = ["<Plugin-ID>", "<Plugin-ID>"]
"RR" = ["<Plugin-ID>", "<Plugin-ID>"]
}
}
```

> [!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.
Expand Down
34 changes: 34 additions & 0 deletions registry/coder/modules/jetbrains/jetbrains.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
42 changes: 41 additions & 1 deletion registry/coder/modules/jetbrains/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
}
}
}
118 changes: 118 additions & 0 deletions registry/coder/modules/jetbrains/script/install_plugins.sh
Original file line number Diff line number Diff line change
@@ -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"