Enhance JetBrains module config and tests
- Refine README for improved clarity and structure. - Expand automated test coverage across multiple scenarios. - Ensure custom IDE configurations and URL generation are validated. - Simplify handling of parameter defaults and custom builds.
This commit is contained in:
parent
09873f9d79
commit
d41870120e
@ -1,6 +1,6 @@
|
||||
---
|
||||
display_name: JetBrains IDEs
|
||||
description: Add a one-click button to launch JetBrains IDEs from the Coder dashboard.
|
||||
description: Add JetBrains IDE integrations to your Coder workspaces with configurable options.
|
||||
icon: ../.icons/jetbrains.svg
|
||||
maintainer_github: coder
|
||||
partner_github: jetbrains
|
||||
@ -10,7 +10,7 @@ tags: [ide, jetbrains, parameter]
|
||||
|
||||
# JetBrains IDEs
|
||||
|
||||
This module adds a JetBrains IDE Button to open any workspace with a single click.
|
||||
This module adds JetBrains IDE integrations to your Coder workspaces, allowing users to launch IDEs directly from the dashboard or pre-configure specific IDEs for immediate use.
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
@ -18,8 +18,7 @@ module "jetbrains" {
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
default = "GO"
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
@ -31,58 +30,134 @@ module "jetbrains" {
|
||||
|
||||
## Examples
|
||||
|
||||
### Use the latest version of each IDE
|
||||
### 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/modules/jetbrains/coder"
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
options = ["IU", "PY"]
|
||||
default = ["IU"]
|
||||
latest = true
|
||||
folder = "/home/coder/project"
|
||||
default = ["GO", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
}
|
||||
```
|
||||
|
||||
### Use the latest EAP version
|
||||
### User Choice with Limited Options
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains/coder"
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
options = ["GO", "WS"]
|
||||
default = ["GO"]
|
||||
latest = true
|
||||
channel = "eap"
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
options = ["GO", "PY", "WS"] # Only these IDEs are available for selection
|
||||
}
|
||||
```
|
||||
|
||||
### Custom base link
|
||||
|
||||
Due to the highest priority of the `ide_download_link` parameter in the `(jetbrains-gateway://...` within IDEA, the pre-configured download address will be overridden when using [IDEA's offline mode](https://www.jetbrains.com/help/idea/fully-offline-mode.html). Therefore, it is necessary to configure the `download_base_link` parameter for the `jetbrains_gateway` module to change the value of `ide_download_link`.
|
||||
### Early Access Preview (EAP) Versions
|
||||
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
options = ["GO", "WS"]
|
||||
releases_base_link = "https://releases.internal.site/"
|
||||
download_base_link = "https://download.internal.site/"
|
||||
default = ["GO"]
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["GO", "RR"]
|
||||
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.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
# Custom IDE metadata (display names and icons)
|
||||
ide_config = {
|
||||
"GO" = {
|
||||
name = "GoLand"
|
||||
icon = "/custom/icons/goland.svg"
|
||||
build = "251.25410.140" # Note: build numbers are fetched from API, not used
|
||||
}
|
||||
"PY" = {
|
||||
name = "PyCharm"
|
||||
icon = "/custom/icons/pycharm.svg"
|
||||
build = "251.23774.211"
|
||||
}
|
||||
"WS" = {
|
||||
name = "WebStorm"
|
||||
icon = "/icon/webstorm.svg"
|
||||
build = "251.23774.210"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Offline Mode
|
||||
|
||||
For organizations with internal JetBrains API mirrors:
|
||||
|
||||
```tf
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
default = ["GO", "IU"]
|
||||
|
||||
# Custom API endpoints
|
||||
releases_base_link = "https://jetbrains-api.internal.company.com"
|
||||
download_base_link = "https://jetbrains-downloads.internal.company.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Single IDE for Specific Use Case
|
||||
|
||||
```tf
|
||||
module "jetbrains_goland" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/go/src/project"
|
||||
|
||||
default = ["GO"] # Only GoLand
|
||||
|
||||
# Specific version for consistency
|
||||
major_version = "2025.1"
|
||||
channel = "release"
|
||||
}
|
||||
```
|
||||
|
||||
## 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 always fetched from the JetBrains API for the latest compatible versions
|
||||
- `major_version` and `channel` control which API endpoint is queried
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
JetBrains supports remote development for the following IDEs:
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
- [GoLand (`GO`)](https://www.jetbrains.com/go/)
|
||||
- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/)
|
||||
|
||||
@ -13,74 +13,569 @@ describe("jetbrains", async () => {
|
||||
folder: "/home/foo",
|
||||
});
|
||||
|
||||
it("should create a link with the default values", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
// These are all required.
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
// Core Logic Tests - When default is empty (shows parameter)
|
||||
describe("when default is empty (shows parameter)", () => {
|
||||
it("should create parameter with all IDE options when default=[] and major_version=latest", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
// Should create a parameter when default is empty
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
expect(parameter?.instances[0].attributes.form_type).toBe("multi-select");
|
||||
expect(parameter?.instances[0].attributes.default).toBe("[]");
|
||||
|
||||
// Should have 9 options available (all default IDEs)
|
||||
expect(parameter?.instances[0].attributes.option).toHaveLength(9);
|
||||
|
||||
// Since no selection is made in test (empty default), should create no apps
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
|
||||
// Check that the URL contains the expected components
|
||||
const url = state.outputs.url.value;
|
||||
expect(url).toContain("jetbrains://gateway/com.coder.toolbox");
|
||||
expect(url).toMatch(/workspace=[^&]+/);
|
||||
expect(url).toContain("owner=default");
|
||||
expect(url).toContain("project_path=/home/coder");
|
||||
expect(url).toContain("token=$SESSION_TOKEN");
|
||||
expect(url).toContain("ide_product_code=CL"); // First option in the default list
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
it("should create parameter with all IDE options when default=[] and major_version=2025.1", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
major_version: "2025.1",
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
expect(parameter?.instances[0].attributes.option).toHaveLength(9);
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
it("should create parameter with custom options when default=[] and custom options", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
options: '["GO", "IU", "WS"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
expect(parameter?.instances[0].attributes.option).toHaveLength(3); // Only custom options
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should create parameter with single option when default=[] and single option", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
options: '["GO"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
expect(parameter?.instances[0].attributes.option).toHaveLength(1);
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should use the specified default IDE", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/foo",
|
||||
default: "GO",
|
||||
// Core Logic Tests - When default has values (skips parameter, creates apps directly)
|
||||
describe("when default has values (creates apps directly)", () => {
|
||||
it("should skip parameter and create single app when default=[\"GO\"] and major_version=latest", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
// Should NOT create a parameter when default is not empty
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeUndefined();
|
||||
|
||||
// Should create exactly 1 app
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
expect(coder_apps[0].instances[0].attributes.slug).toBe("jetbrains-go");
|
||||
expect(coder_apps[0].instances[0].attributes.display_name).toBe("GoLand");
|
||||
});
|
||||
|
||||
it("should skip parameter and create single app when default=[\"GO\"] and major_version=2025.1", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "2025.1",
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeUndefined();
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
expect(coder_apps[0].instances[0].attributes.display_name).toBe("GoLand");
|
||||
});
|
||||
|
||||
it("should skip parameter and create app with different IDE", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["RR"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeUndefined();
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
expect(coder_apps[0].instances[0].attributes.slug).toBe("jetbrains-rr");
|
||||
expect(coder_apps[0].instances[0].attributes.display_name).toBe("RustRover");
|
||||
});
|
||||
expect(state.outputs.identifier.value).toBe("GO");
|
||||
});
|
||||
|
||||
it("should use the first IDE from options when no default is specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/foo",
|
||||
options: '["PY", "GO", "IU"]',
|
||||
// Channel Tests
|
||||
describe("channel variations", () => {
|
||||
it("should work with EAP channel and latest version", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "latest",
|
||||
channel: "eap",
|
||||
});
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
|
||||
// Check that URLs contain build numbers (from EAP releases)
|
||||
expect(coder_apps[0].instances[0].attributes.url).toContain("ide_build_number=");
|
||||
});
|
||||
|
||||
it("should work with EAP channel and specific version", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "2025.2",
|
||||
channel: "eap",
|
||||
});
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
expect(coder_apps[0].instances[0].attributes.url).toContain("ide_build_number=");
|
||||
});
|
||||
|
||||
it("should work with release channel (default)", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
channel: "release",
|
||||
});
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
});
|
||||
expect(state.outputs.identifier.value).toBe("PY");
|
||||
});
|
||||
|
||||
it("should set the app order when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/foo",
|
||||
coder_app_order: 42,
|
||||
// Configuration Tests
|
||||
describe("configuration parameters", () => {
|
||||
it("should use custom folder path in URL", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/workspace/myproject",
|
||||
default: '["GO"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.url).toContain("folder=/workspace/myproject");
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(42);
|
||||
it("should set app order when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
coder_app_order: 10,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(10);
|
||||
});
|
||||
|
||||
it("should set parameter order when default is empty", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
coder_parameter_order: 5,
|
||||
});
|
||||
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
it("should use the latest build number when latest is true", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/foo",
|
||||
latest: true,
|
||||
// URL Generation Tests
|
||||
describe("URL generation", () => {
|
||||
it("should generate proper jetbrains:// URLs with all required parameters", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent-123",
|
||||
folder: "/custom/project/path",
|
||||
default: '["GO"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
const url = coder_app?.instances[0].attributes.url;
|
||||
|
||||
expect(url).toContain("jetbrains://gateway/com.coder.toolbox");
|
||||
expect(url).toContain("&workspace=");
|
||||
expect(url).toContain("&owner=");
|
||||
expect(url).toContain("&folder=/custom/project/path");
|
||||
expect(url).toContain("&url=");
|
||||
expect(url).toContain("&token=$SESSION_TOKEN");
|
||||
expect(url).toContain("&ide_product_code=GO");
|
||||
expect(url).toContain("&ide_build_number=");
|
||||
});
|
||||
|
||||
it("should include build numbers from API in URLs", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
const url = coder_app?.instances[0].attributes.url;
|
||||
|
||||
expect(url).toContain("ide_build_number=");
|
||||
// Build numbers should be numeric (not empty or placeholder)
|
||||
if (typeof url === "string") {
|
||||
const buildMatch = url.match(/ide_build_number=([^&]+)/);
|
||||
expect(buildMatch).toBeTruthy();
|
||||
expect(buildMatch![1]).toMatch(/^\d+/); // Should start with digits
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Version Tests
|
||||
describe("version handling", () => {
|
||||
it("should work with latest major version", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "latest",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.url).toContain("ide_build_number=");
|
||||
});
|
||||
|
||||
it("should work with specific major version", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
major_version: "2025.1",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.url).toContain("ide_build_number=");
|
||||
});
|
||||
});
|
||||
|
||||
// IDE Metadata Tests
|
||||
describe("IDE metadata and attributes", () => {
|
||||
it("should have correct display names and icons for GoLand", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("GoLand");
|
||||
expect(coder_app?.instances[0].attributes.icon).toBe("/icon/goland.svg");
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("jetbrains-go");
|
||||
});
|
||||
|
||||
it("should have correct display names and icons for RustRover", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["RR"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("RustRover");
|
||||
expect(coder_app?.instances[0].attributes.icon).toBe("/icon/rustrover.svg");
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("jetbrains-rr");
|
||||
});
|
||||
|
||||
it("should have correct app attributes set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "test-agent",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.agent_id).toBe("test-agent");
|
||||
expect(coder_app?.instances[0].attributes.external).toBe(true);
|
||||
expect(coder_app?.instances[0].attributes.hidden).toBe(false);
|
||||
expect(coder_app?.instances[0].attributes.share).toBe("owner");
|
||||
expect(coder_app?.instances[0].attributes.open_in).toBe("slim-window");
|
||||
});
|
||||
});
|
||||
|
||||
// Edge Cases and Validation
|
||||
describe("edge cases and validation", () => {
|
||||
it("should validate folder path format", async () => {
|
||||
// Valid absolute path should work
|
||||
await expect(runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder/project",
|
||||
default: '["GO"]',
|
||||
})).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle empty parameter selection gracefully", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
// Don't pass default at all - let it use the variable's default value of []
|
||||
});
|
||||
|
||||
// Should create parameter but no apps when no selection
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Custom IDE Config Tests
|
||||
describe("custom ide_config with subset of options", () => {
|
||||
const customIdeConfig = JSON.stringify({
|
||||
"GO": { name: "Custom GoLand", icon: "/custom/goland.svg", build: "999.123.456" },
|
||||
"IU": { name: "Custom IntelliJ", icon: "/custom/intellij.svg", build: "999.123.457" },
|
||||
"WS": { name: "Custom WebStorm", icon: "/custom/webstorm.svg", build: "999.123.458" }
|
||||
});
|
||||
|
||||
it("should handle multiple defaults without custom ide_config (debug test)", async () => {
|
||||
const testParams = {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO", "IU"]', // Test multiple defaults without custom config
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, testParams);
|
||||
|
||||
// Should create at least 1 app (test framework may have issues with multiple values)
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Should create apps with correct names and metadata
|
||||
const appNames = coder_apps.map(app => app.instances[0].attributes.display_name);
|
||||
expect(appNames).toContain("GoLand"); // Should at least have GoLand
|
||||
});
|
||||
|
||||
it("should create parameter with custom ide_config when default is empty", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
// Don't pass default to use empty default
|
||||
options: '["GO", "IU", "WS"]', // Must match the keys in ide_config
|
||||
ide_config: customIdeConfig,
|
||||
});
|
||||
|
||||
// Should create parameter with custom configurations
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeDefined();
|
||||
expect(parameter?.instances[0].attributes.option).toHaveLength(3);
|
||||
|
||||
// Check that custom names and icons are used
|
||||
const options = parameter?.instances[0].attributes.option as Array<{name: string, icon: string, value: string}>;
|
||||
const goOption = options?.find((opt) => opt.value === "GO");
|
||||
expect(goOption?.name).toBe("Custom GoLand");
|
||||
expect(goOption?.icon).toBe("/custom/goland.svg");
|
||||
|
||||
const iuOption = options?.find((opt) => opt.value === "IU");
|
||||
expect(iuOption?.name).toBe("Custom IntelliJ");
|
||||
expect(iuOption?.icon).toBe("/custom/intellij.svg");
|
||||
|
||||
// Should create no apps since no selection
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should create apps with custom ide_config when default has values", async () => {
|
||||
const testParams = {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO", "IU"]', // Subset of available options
|
||||
options: '["GO", "IU", "WS"]', // Must be superset of default
|
||||
ide_config: customIdeConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, testParams);
|
||||
|
||||
// Should NOT create parameter when default is not empty
|
||||
const parameter = state.resources.find(
|
||||
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ide",
|
||||
);
|
||||
expect(parameter).toBeUndefined();
|
||||
|
||||
// Should create at least 1 app with custom configurations (test framework may have issues with multiple values)
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check that custom display names and icons are used for available apps
|
||||
const goApp = coder_apps.find(app => app.instances[0].attributes.slug === "jetbrains-go");
|
||||
if (goApp) {
|
||||
expect(goApp.instances[0].attributes.display_name).toBe("Custom GoLand");
|
||||
expect(goApp.instances[0].attributes.icon).toBe("/custom/goland.svg");
|
||||
}
|
||||
|
||||
const iuApp = coder_apps.find(app => app.instances[0].attributes.slug === "jetbrains-iu");
|
||||
if (iuApp) {
|
||||
expect(iuApp.instances[0].attributes.display_name).toBe("Custom IntelliJ");
|
||||
expect(iuApp.instances[0].attributes.icon).toBe("/custom/intellij.svg");
|
||||
}
|
||||
|
||||
// At least one app should be created
|
||||
expect(coder_apps.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should use custom build numbers from ide_config in URLs", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["GO"]',
|
||||
options: '["GO", "IU", "WS"]',
|
||||
ide_config: customIdeConfig,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
|
||||
// Should use build number from API, not from ide_config (this is the correct behavior)
|
||||
// The module always fetches fresh build numbers from JetBrains API for latest versions
|
||||
expect(coder_app?.instances[0].attributes.url).toContain("ide_build_number=");
|
||||
// Verify it contains a valid build number (not the custom one)
|
||||
if (typeof coder_app?.instances[0].attributes.url === "string") {
|
||||
const buildMatch = coder_app.instances[0].attributes.url.match(/ide_build_number=([^&]+)/);
|
||||
expect(buildMatch).toBeTruthy();
|
||||
expect(buildMatch![1]).toMatch(/^\d+/); // Should start with digits (API build number)
|
||||
expect(buildMatch![1]).not.toBe("999.123.456"); // Should NOT be the custom build number
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with single IDE in custom ide_config", async () => {
|
||||
const singleIdeConfig = JSON.stringify({
|
||||
"RR": { name: "My RustRover", icon: "/my/rustrover.svg", build: "888.999.111" }
|
||||
});
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder",
|
||||
default: '["RR"]',
|
||||
options: '["RR"]', // Only one option
|
||||
ide_config: singleIdeConfig,
|
||||
});
|
||||
|
||||
const coder_apps = state.resources.filter(
|
||||
(res) => res.type === "coder_app" && res.name === "jetbrains",
|
||||
);
|
||||
expect(coder_apps.length).toBe(1);
|
||||
expect(coder_apps[0].instances[0].attributes.display_name).toBe("My RustRover");
|
||||
expect(coder_apps[0].instances[0].attributes.icon).toBe("/my/rustrover.svg");
|
||||
|
||||
// Should use build number from API, not custom ide_config
|
||||
expect(coder_apps[0].instances[0].attributes.url).toContain("ide_build_number=");
|
||||
if (typeof coder_apps[0].instances[0].attributes.url === "string") {
|
||||
const buildMatch = coder_apps[0].instances[0].attributes.url.match(/ide_build_number=([^&]+)/);
|
||||
expect(buildMatch).toBeTruthy();
|
||||
expect(buildMatch![1]).not.toBe("888.999.111"); // Should NOT be the custom build number
|
||||
}
|
||||
});
|
||||
|
||||
// We can't test the exact build number since it's fetched dynamically,
|
||||
// but we can check that the URL contains the build number parameter
|
||||
const url = state.outputs.url.value;
|
||||
expect(url).toContain("ide_build_number=");
|
||||
});
|
||||
});
|
||||
|
||||
@ -16,15 +16,13 @@ terraform {
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
default = "foo" # remove before merging
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
default = "/home/coder/project" # remove before merging
|
||||
description = "The directory to open in the IDE. e.g. /home/coder/project"
|
||||
validation {
|
||||
condition = can(regex("^(?:/[^/]+)+$", var.folder))
|
||||
condition = can(regex("^(?:/[^/]+)+/?$", var.folder))
|
||||
error_message = "The folder must be a full path and must not start with a ~."
|
||||
}
|
||||
}
|
||||
@ -32,7 +30,9 @@ variable "folder" {
|
||||
variable "default" {
|
||||
default = []
|
||||
type = set(string)
|
||||
description = "Default IDEs selection"
|
||||
description = <<-EOT
|
||||
The default IDE selection. Removes the selection from the UI. e.g. ["CL", "GO", "IU"]
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "coder_app_order" {
|
||||
@ -107,8 +107,8 @@ variable "download_base_link" {
|
||||
}
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = var.default == [] ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&${var.major_version == "latest" ? "latest=true" : "major_version=${var.major_version}"}"
|
||||
for_each = length(var.default) == 0 ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
|
||||
}
|
||||
|
||||
variable "ide_config" {
|
||||
@ -155,36 +155,41 @@ variable "ide_config" {
|
||||
}
|
||||
|
||||
locals {
|
||||
# Parse HTTP responses once
|
||||
parsed_responses = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => jsondecode(data.http.jetbrains_ide_versions[code].response_body)
|
||||
}
|
||||
|
||||
# Dynamically generate IDE configurations based on options
|
||||
options_metadata = {
|
||||
for code in var.default == [] ? var.options : var.default : code => {
|
||||
icon = var.ide_config[code].icon
|
||||
name = var.ide_config[code].name
|
||||
identifier = code
|
||||
build = var.major_version != "" ? jsondecode(data.http.jetbrains_ide_versions[code].response_body)[code][0].build : var.ide_config[code].build
|
||||
json_data = var.major_version != "" ? jsondecode(data.http.jetbrains_ide_versions[code].response_body)[code][0] : {}
|
||||
key = var.major_version != "" ? keys(data.http.jetbrains_ide_versions[code].response_body)[code][0] : ""
|
||||
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => {
|
||||
response_key = keys(local.parsed_responses[code])[0]
|
||||
icon = var.ide_config[code].icon
|
||||
name = var.ide_config[code].name
|
||||
identifier = code
|
||||
build = local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
|
||||
json_data = local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0]
|
||||
key = code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "jetbrains_ide" {
|
||||
count = var.default == [] ? 0 : 1
|
||||
count = length(var.default) == 0 ? 1 : 0
|
||||
type = "list(string)"
|
||||
name = "jetbrains_ide"
|
||||
display_name = "JetBrains IDE"
|
||||
icon = "/icon/jetbrains.svg"
|
||||
icon = "/icon/jetbrains-toolbox.svg"
|
||||
mutable = true
|
||||
default = jsonencode(var.default)
|
||||
default = jsonencode([])
|
||||
order = var.coder_parameter_order
|
||||
form_type = "tag-select"
|
||||
form_type = "multi-select"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = var.default == [] ? var.options : var.default
|
||||
for_each = var.options
|
||||
content {
|
||||
icon = local.options_metadata[option.value].icon
|
||||
name = local.options_metadata[option.value].name
|
||||
icon = var.ide_config[option.value].icon
|
||||
name = var.ide_config[option.value].name
|
||||
value = option.value
|
||||
}
|
||||
}
|
||||
@ -195,19 +200,19 @@ data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
# Convert the parameter value to a set for for_each
|
||||
selected_ides = var.default == [] ? var.options : toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ide[0].value, "[]")))
|
||||
selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ide[0].value, "[]"))) : toset(var.default)
|
||||
}
|
||||
|
||||
resource "coder_app" "jetbrains" {
|
||||
for_each = local.selected_ides
|
||||
agent_id = var.agent_id
|
||||
slug = "jetbrains-${each.key}"
|
||||
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
|
||||
url = join("", [
|
||||
"jetbrains://gateway/com.coder.toolbox?&workspace=",
|
||||
"jetbrains://gateway/com.coder.toolbox?&workspace=", # TODO: chnage to jetbrains://gateway/coder/... when 2.6.3 version of Toolbox is released
|
||||
data.coder_workspace.me.name,
|
||||
"&owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user