feature (jetbrains-plugins): add module for installing jetbrains plugin (#772)
Co-authored-by: DevCats <christofer@coder.com> Co-authored-by: DevCats <chris@dualriver.com>
This commit is contained in:
parent
b72577707c
commit
b108185c14
BIN
registry/harsh9485/.images/avatar.png
Normal file
BIN
registry/harsh9485/.images/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
11
registry/harsh9485/README.md
Normal file
11
registry/harsh9485/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
display_name: Harsh Singh Panwar
|
||||||
|
bio: Open source contributor
|
||||||
|
github: Harsh9485
|
||||||
|
avatar: ./.images/avatar.png
|
||||||
|
status: community
|
||||||
|
---
|
||||||
|
|
||||||
|
# Harsh Singh Panwar
|
||||||
|
|
||||||
|
Community modules for Coder workspaces.
|
||||||
80
registry/harsh9485/modules/jetbrains-plugins/README.md
Normal file
80
registry/harsh9485/modules/jetbrains-plugins/README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
display_name: JetBrains Plugin Installer
|
||||||
|
description: Companion module for coder/jetbrains that automatically installs JetBrains Marketplace plugins.
|
||||||
|
icon: ../../../../.icons/jetbrains.svg
|
||||||
|
tags: [ide, jetbrains, plugins]
|
||||||
|
---
|
||||||
|
|
||||||
|
# JetBrains Plugin Installer
|
||||||
|
|
||||||
|
A companion module for
|
||||||
|
[coder/jetbrains](https://registry.coder.com/modules/jetbrains) that
|
||||||
|
automatically installs JetBrains Marketplace plugins into your workspace.
|
||||||
|
|
||||||
|
Use this alongside the core `coder/jetbrains` module — it handles plugin
|
||||||
|
installation while `coder/jetbrains` handles IDE setup and Toolbox
|
||||||
|
integration.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jetbrains_plugins" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/harsh9485/jetbrains-plugins/coder"
|
||||||
|
version = "0.1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
|
||||||
|
jetbrains_plugins = {
|
||||||
|
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- The [coder/jetbrains](https://registry.coder.com/modules/jetbrains)
|
||||||
|
module (or equivalent JetBrains Toolbox setup) must already be
|
||||||
|
configured in your template.
|
||||||
|
- `jq` must be available on `PATH`.
|
||||||
|
- Linux environment only.
|
||||||
|
|
||||||
|
## Finding Plugin IDs
|
||||||
|
|
||||||
|
Open the plugin page on the
|
||||||
|
[JetBrains Marketplace](https://plugins.jetbrains.com/). Scroll to
|
||||||
|
**Additional Information** and copy the **Plugin ID**.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jetbrains" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder/jetbrains/coder"
|
||||||
|
version = "1.4.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
folder = "/home/coder/project"
|
||||||
|
default = ["PY", "GO"]
|
||||||
|
}
|
||||||
|
|
||||||
|
module "jetbrains_plugins" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/harsh9485/jetbrains-plugins/coder"
|
||||||
|
version = "0.1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
|
||||||
|
jetbrains_plugins = {
|
||||||
|
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
|
||||||
|
"GO" = ["org.jetbrains.plugins.go-template"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The keys in `jetbrains_plugins` are IDE product codes (`PY`, `GO`, `IU`,
|
||||||
|
etc.) matching the codes used by the `coder/jetbrains` module. Each value
|
||||||
|
is a list of Marketplace plugin IDs to install for that IDE.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> After installing the IDE, restart the workspace. On the next start the
|
||||||
|
> module detects installed IDEs and automatically installs the configured
|
||||||
|
> plugins.
|
||||||
|
|
||||||
|
Some plugins may be disabled by default due to JetBrains security
|
||||||
|
defaults — you might need to enable them manually in the IDE.
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
run "no_script_when_plugins_empty" {
|
||||||
|
command = plan
|
||||||
|
|
||||||
|
variables {
|
||||||
|
agent_id = "foo"
|
||||||
|
jetbrains_plugins = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
condition = length(resource.coder_script.install_jetbrains_plugins) == 0
|
||||||
|
error_message = "Expected no plugin install script when plugins map is empty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run "script_created_when_plugins_provided" {
|
||||||
|
command = plan
|
||||||
|
|
||||||
|
variables {
|
||||||
|
agent_id = "foo"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run "rejects_invalid_product_code" {
|
||||||
|
command = plan
|
||||||
|
|
||||||
|
variables {
|
||||||
|
agent_id = "foo"
|
||||||
|
jetbrains_plugins = {
|
||||||
|
"INVALID" = ["com.example.plugin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_failures = [
|
||||||
|
var.jetbrains_plugins,
|
||||||
|
]
|
||||||
|
}
|
||||||
59
registry/harsh9485/modules/jetbrains-plugins/main.tf
Normal file
59
registry/harsh9485/modules/jetbrains-plugins/main.tf
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.9"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The resource ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue([
|
||||||
|
for code in keys(var.jetbrains_plugins) : contains(
|
||||||
|
["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code
|
||||||
|
)
|
||||||
|
])
|
||||||
|
error_message = "Keys must be valid JetBrains product codes: CL, GO, IU, PS, PY, RD, RM, RR, WS."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins))
|
||||||
|
plugin_install_script = file("${path.module}/scripts/install_plugins.sh")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '${base64encode(local.plugin_install_script)}' | base64 -d > /tmp/install_plugins.sh
|
||||||
|
chmod +x /tmp/install_plugins.sh
|
||||||
|
|
||||||
|
/tmp/install_plugins.sh
|
||||||
|
EOT
|
||||||
|
}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOGFILE="$HOME/.config/JetBrains/install_plugins.log"
|
||||||
|
TOOLBOX_BASE="$HOME/.local/share/JetBrains/Toolbox/apps"
|
||||||
|
PLUGIN_MAP_FILE="$HOME/.config/JetBrains/plugins.json"
|
||||||
|
PLUGIN_ALREADY_INSTALLED_MAP="$HOME/.config/JetBrains"
|
||||||
|
|
||||||
|
# Verify jq is available
|
||||||
|
if ! command -v jq > /dev/null 2>&1; then
|
||||||
|
echo "Error: 'jq' is required but not installed. Please install it manually." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOGFILE")"
|
||||||
|
|
||||||
|
exec > >(tee -a "$LOGFILE") 2>&1
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s %s\n' "$(date --iso-8601=seconds)" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Read plugin JSON --------
|
||||||
|
get_enabled_codes() {
|
||||||
|
jq -r 'keys[]' "$PLUGIN_MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_plugins_for_code() {
|
||||||
|
jq -r --arg CODE "$1" '.[$CODE][]?' "$PLUGIN_MAP_FILE" 2> /dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns only plugins that are NOT already installed
|
||||||
|
check_plugins_installed() {
|
||||||
|
local code="$1"
|
||||||
|
shift
|
||||||
|
local plugins=("$@")
|
||||||
|
|
||||||
|
local installed_file="$PLUGIN_ALREADY_INSTALLED_MAP/${code}_installed.json"
|
||||||
|
|
||||||
|
# If no installed file exists, all plugins need to be installed
|
||||||
|
if [ ! -f "$installed_file" ]; then
|
||||||
|
printf '%s\n' "${plugins[@]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
installed_plugins=$(jq -r '.[]?' "$installed_file" 2> /dev/null)
|
||||||
|
|
||||||
|
for plugin in "${plugins[@]}"; do
|
||||||
|
if ! echo "$installed_plugins" | grep -Fxq "$plugin"; then
|
||||||
|
echo "$plugin"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Product code mapping --------
|
||||||
|
map_folder_to_code() {
|
||||||
|
case "$1" in
|
||||||
|
*pycharm*) echo "PY" ;;
|
||||||
|
*idea*) echo "IU" ;;
|
||||||
|
*webstorm*) echo "WS" ;;
|
||||||
|
*goland*) echo "GO" ;;
|
||||||
|
*clion*) echo "CL" ;;
|
||||||
|
*phpstorm*) echo "PS" ;;
|
||||||
|
*rider*) echo "RD" ;;
|
||||||
|
*rubymine*) echo "RM" ;;
|
||||||
|
*rustrover*) echo "RR" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- CLI launcher names --------
|
||||||
|
launcher_for_code() {
|
||||||
|
case "$1" in
|
||||||
|
PY) echo "pycharm" ;;
|
||||||
|
IU) echo "idea" ;;
|
||||||
|
WS) echo "webstorm" ;;
|
||||||
|
GO) echo "goland" ;;
|
||||||
|
CL) echo "clion" ;;
|
||||||
|
PS) echo "phpstorm" ;;
|
||||||
|
RD) echo "rider" ;;
|
||||||
|
RM) echo "rubymine" ;;
|
||||||
|
RR) echo "rustrover" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
find_cli_launcher() {
|
||||||
|
local exe
|
||||||
|
exe="$(launcher_for_code "$1")" || return 1
|
||||||
|
|
||||||
|
# Look for the newest version directory
|
||||||
|
local latest_version
|
||||||
|
latest_version=$(find "$2" -maxdepth 2 -type d -name "ch-*" 2> /dev/null | sort -V | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$latest_version" ] && [ -f "$latest_version/bin/$exe" ]; then
|
||||||
|
echo "$latest_version/bin/$exe"
|
||||||
|
elif [ -f "$2/bin/$exe" ]; then
|
||||||
|
echo "$2/bin/$exe"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Marks a plugin as installed by adding it to the installed plugins JSON file
|
||||||
|
mark_plugins_installed() {
|
||||||
|
local code="$1"
|
||||||
|
local plugin="$2"
|
||||||
|
|
||||||
|
local installed_file="$PLUGIN_ALREADY_INSTALLED_MAP/${code}_installed.json"
|
||||||
|
|
||||||
|
mkdir -p "$PLUGIN_ALREADY_INSTALLED_MAP"
|
||||||
|
|
||||||
|
# Create file with empty array if it doesn't exist
|
||||||
|
if [ ! -f "$installed_file" ]; then
|
||||||
|
echo '[]' > "$installed_file" || {
|
||||||
|
log "Error: Failed to create $installed_file"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq --arg PLUGIN "$plugin" '. += [$PLUGIN]' "$installed_file" > "${installed_file}.tmp" 2> /dev/null \
|
||||||
|
&& mv "${installed_file}.tmp" "$installed_file" || {
|
||||||
|
log "Error: Failed to update $installed_file with plugin $plugin"
|
||||||
|
rm -f "${installed_file}.tmp"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
log "Marked plugin as installed: $plugin"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install_plugin() {
|
||||||
|
log "Installing plugin: $2"
|
||||||
|
if "$1" installPlugins "$2"; then
|
||||||
|
log "Successfully installed plugin: $2"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "Failed to install plugin: $2"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- Main --------
|
||||||
|
log "Plugin installer started"
|
||||||
|
|
||||||
|
if [ ! -f "$PLUGIN_MAP_FILE" ]; then
|
||||||
|
log "No plugins.json found. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$TOOLBOX_BASE" ]; then
|
||||||
|
log "Toolbox directory not found. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load list of IDE codes user actually needs
|
||||||
|
mapfile -t pending_codes < <(get_enabled_codes)
|
||||||
|
|
||||||
|
if [ ${#pending_codes[@]} -eq 0 ]; then
|
||||||
|
log "No plugin entries found. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Waiting for IDE installation. Pending codes: ${pending_codes[*]}"
|
||||||
|
|
||||||
|
# Loop until all plugins installed
|
||||||
|
for product_dir in "$TOOLBOX_BASE"/*; do
|
||||||
|
[ -d "$product_dir" ] || continue
|
||||||
|
|
||||||
|
product_name="$(basename "$product_dir")"
|
||||||
|
code="$(map_folder_to_code "$product_name")"
|
||||||
|
|
||||||
|
# Only process codes user requested
|
||||||
|
if [[ ! " ${pending_codes[*]} " =~ " $code " ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store plugins as array for consistency
|
||||||
|
mapfile -t plugins_list < <(get_plugins_for_code "$code")
|
||||||
|
if [ ${#plugins_list[@]} -eq 0 ]; then
|
||||||
|
log "No plugins for $code"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get only plugins that are not already installed
|
||||||
|
mapfile -t new_plugins < <(check_plugins_installed "$code" "${plugins_list[@]}")
|
||||||
|
if [ ${#new_plugins[@]} -eq 0 ]; then
|
||||||
|
log "All plugins for $code are already installed"
|
||||||
|
# Remove code from pending list since all plugins are installed
|
||||||
|
tmp=()
|
||||||
|
for c in "${pending_codes[@]}"; do
|
||||||
|
[ "$c" != "$code" ] && tmp+=("$c")
|
||||||
|
done
|
||||||
|
pending_codes=("${tmp[@]}")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
cli_launcher_path="$(find_cli_launcher "$code" "$product_dir")" || continue
|
||||||
|
log "Detected IDE $code at $product_dir"
|
||||||
|
log "Plugins to install for $code: ${#new_plugins[@]} plugin(s)"
|
||||||
|
|
||||||
|
# Install only the plugins that are not yet installed
|
||||||
|
for plugin in "${new_plugins[@]}"; do
|
||||||
|
if install_plugin "$cli_launcher_path" "$plugin"; then
|
||||||
|
# Mark plugin as installed after successful installation
|
||||||
|
mark_plugins_installed "$code" "$plugin"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# remove code from pending list after success
|
||||||
|
tmp=()
|
||||||
|
for c in "${pending_codes[@]}"; do
|
||||||
|
[ "$c" != "$code" ] && tmp+=("$c")
|
||||||
|
done
|
||||||
|
pending_codes=("${tmp[@]}")
|
||||||
|
log "Finished $code. Remaining: ${pending_codes[*]:-none}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#pending_codes[@]} -gt 0 ]; then
|
||||||
|
log "These IDEs not found: ${pending_codes[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Exiting."
|
||||||
Loading…
x
Reference in New Issue
Block a user