diff --git a/.icons/fleet.svg b/.icons/fleet.svg new file mode 100644 index 00000000..ba910eb9 --- /dev/null +++ b/.icons/fleet.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/coder/modules/jetbrains-fleet/README.md b/registry/coder/modules/jetbrains-fleet/README.md new file mode 100644 index 00000000..16f0f411 --- /dev/null +++ b/registry/coder/modules/jetbrains-fleet/README.md @@ -0,0 +1,81 @@ +--- +display_name: JetBrains Fleet +description: Add a one-click button to launch JetBrains Fleet to connect to your workspace. +icon: ../../../../.icons/jetbrains.svg +verified: true +tags: [ide, jetbrains, fleet] +--- + +# Jetbrains Fleet + +This module adds a Jetbrains Fleet button to your Coder workspace that opens the workspace in JetBrains Fleet using SSH remote development. + +JetBrains Fleet is a next-generation IDE that supports collaborative development and distributed architectures. It connects to your Coder workspace via SSH, providing a seamless remote development experience. + +```tf +module "jetbrains_fleet" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-fleet/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Requirements + +- JetBrains Fleet must be installed locally on your development machine +- Download Fleet from: https://www.jetbrains.com/fleet/ + +> [!IMPORTANT] +> Fleet needs you to either have Coder CLI installed with `coder config-ssh` run or [Coder Desktop](https://coder.com/docs/user-guides/desktop). + +## Examples + +### Basic usage + +```tf +module "jetbrains_fleet" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-fleet/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Open a specific folder + +```tf +module "jetbrains_fleet" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-fleet/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" +} +``` + +### Customize app name and grouping + +```tf +module "jetbrains_fleet" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-fleet/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + display_name = "Fleet" + group = "JetBrains IDEs" + order = 1 +} +``` + +### With custom agent name + +```tf +module "jetbrains_fleet" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-fleet/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + agent_name = coder_agent.example.name +} +``` diff --git a/registry/coder/modules/jetbrains-fleet/main.test.ts b/registry/coder/modules/jetbrains-fleet/main.test.ts new file mode 100644 index 00000000..b9463e81 --- /dev/null +++ b/registry/coder/modules/jetbrains-fleet/main.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("jetbrains-fleet", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + expect(state.outputs.fleet_url.value).toBe( + "fleet://fleet.ssh/default.coder", + ); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "fleet", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBeNull(); + }); + + it("adds folder", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + }); + expect(state.outputs.fleet_url.value).toBe( + "fleet://fleet.ssh/default.coder?pwd=/foo/bar", + ); + }); + + it("adds agent_name to hostname", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + agent_name: "myagent", + }); + expect(state.outputs.fleet_url.value).toBe( + "fleet://fleet.ssh/myagent.default.default.coder", + ); + }); + + it("custom display name and slug", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + display_name: "My Fleet", + slug: "my-fleet", + }); + expect(state.outputs.fleet_url.value).toBe( + "fleet://fleet.ssh/default.coder", + ); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "fleet", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances[0].attributes.display_name).toBe("My Fleet"); + expect(coder_app?.instances[0].attributes.slug).toBe("my-fleet"); + }); + + it("expect order to be set", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + order: "22", + }); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "fleet", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBe(22); + }); + + it("expect group to be set", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + group: "JetBrains IDEs", + }); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "fleet", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.group).toBe("JetBrains IDEs"); + }); +}); \ No newline at end of file diff --git a/registry/coder/modules/jetbrains-fleet/main.tf b/registry/coder/modules/jetbrains-fleet/main.tf new file mode 100644 index 00000000..cc2ab740 --- /dev/null +++ b/registry/coder/modules/jetbrains-fleet/main.tf @@ -0,0 +1,81 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "agent_name" { + type = string + description = "The name of the agent" + default = "" +} + +variable "folder" { + type = string + description = "The folder to open in Fleet IDE." + default = "" +} + +variable "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 "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "slug" { + type = string + description = "The slug of the app." + default = "fleet" +} + +variable "display_name" { + type = string + description = "The display name of the app." + default = "JetBrains Fleet" +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + workspace_name = lower(data.coder_workspace.me.name) + owner_name = lower(data.coder_workspace_owner.me.name) + agent_name = lower(var.agent_name) + hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder" +} + +resource "coder_app" "fleet" { + agent_id = var.agent_id + external = true + icon = "/icon/fleet.svg" + slug = var.slug + display_name = var.display_name + order = var.order + group = var.group + url = join("", [ + "fleet://fleet.ssh/", + local.hostname, + var.folder != "" ? join("", ["?pwd=", var.folder]) : "" + ]) +} + +output "fleet_url" { + value = coder_app.fleet.url + description = "Fleet IDE connection URL." +}