diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index bc5b4d3de..0ea2c866b 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -10,13 +10,19 @@ tags: [windows, rdp, web, desktop] Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway). +The module adds an **RDP Desktop** app by default that opens the workspace in a +native RDP client through [Coder Desktop](https://coder.com/docs/user-guides/desktop). +Set `enable_native_rdp = false` for web-only access. The native app uses the same +administrator credentials as the web client and requires `agent_name`. + ```tf # AWS example. See below for examples of using this module with other providers module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.3.1" - agent_id = coder_agent.main.id + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/windows-rdp/coder" + version = "1.4.0" + agent_id = coder_agent.main.id + agent_name = "main" } ``` @@ -30,10 +36,11 @@ module "windows_rdp" { ```tf module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.3.1" - agent_id = coder_agent.main.id + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/windows-rdp/coder" + version = "1.4.0" + agent_id = coder_agent.main.id + agent_name = "main" } ``` @@ -41,10 +48,11 @@ module "windows_rdp" { ```tf module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.3.1" - agent_id = coder_agent.main.id + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/windows-rdp/coder" + version = "1.4.0" + agent_id = coder_agent.main.id + agent_name = "main" } ``` @@ -54,8 +62,24 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.3.1" + version = "1.4.0" agent_id = coder_agent.main.id + agent_name = "main" devolutions_gateway_version = "2025.2.2" # Specify a specific version } ``` + +### With Native RDP + +Coder Desktop must be installed and connected on the local device. The agent +name must match the `coder_agent` resource passed through `agent_id`. + +```tf +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/windows-rdp/coder" + version = "1.4.0" + agent_id = coder_agent.main.id + agent_name = "main" +} +``` diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index f34899c52..cb65cfbdf 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -3,12 +3,12 @@ import { type TerraformState, runTerraformApply, runTerraformInit, - testRequiredVariables, } from "~test"; type TestVariables = Readonly<{ agent_id: string; share?: string; + enable_native_rdp?: boolean; admin_username?: string; admin_password?: string; }>; @@ -67,13 +67,19 @@ function extractFormFieldValues(rdpScript: string): { */ describe("Web RDP", async () => { await runTerraformInit(import.meta.dir); - testRequiredVariables(import.meta.dir, { - agent_id: "foo", + + it("requires agent_id", async () => { + await expect( + runTerraformApply(import.meta.dir, { + enable_native_rdp: false, + }), + ).rejects.toThrow('input variable "agent_id" is not set'); }); it("Has the PowerShell script install Devolutions Gateway", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + enable_native_rdp: false, }); const lines = findWindowsRdpScript(state) @@ -99,6 +105,7 @@ describe("Web RDP", async () => { import.meta.dir, { agent_id: "foo", + enable_native_rdp: false, }, ); @@ -117,6 +124,7 @@ describe("Web RDP", async () => { import.meta.dir, { agent_id: "foo", + enable_native_rdp: false, admin_username: customAdminUsername, admin_password: customAdminPassword, }, @@ -138,6 +146,7 @@ describe("Web RDP", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + enable_native_rdp: false, admin_password: specialPassword, }); diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index c1035ce18..bc530591b 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -53,6 +53,29 @@ variable "agent_id" { description = "The ID of a Coder agent." } +variable "agent_name" { + description = "The name of the Coder agent used for native RDP connections. Required when enable_native_rdp is true." + type = string + default = null + + validation { + condition = !var.enable_native_rdp || var.agent_name != null + error_message = "agent_name must be set when enable_native_rdp is true." + } +} + +variable "enable_native_rdp" { + description = "Add an app button that opens the workspace in a native RDP client through Coder Desktop." + type = bool + default = true +} + +variable "tooltip" { + description = "Markdown text displayed when hovering over the native RDP app." + type = string + default = "You need to install [Coder Desktop](https://coder.com/docs/user-guides/desktop) to use this button." +} + variable "admin_username" { type = string default = "Administrator" @@ -83,6 +106,14 @@ locals { # single quotes keeps values containing $, backticks, or double quotes intact. ps_admin_username = replace(var.admin_username, "'", "''") ps_admin_password = replace(var.admin_password, "'", "''") + + # Terraform still evaluates the disabled resource configuration. This + # placeholder keeps URL interpolation valid when native RDP is disabled. + native_rdp_agent_name = var.agent_name != null ? var.agent_name : "" +} + +data "coder_workspace" "me" { + count = var.enable_native_rdp ? 1 : 0 } resource "coder_script" "windows-rdp" { @@ -125,6 +156,20 @@ resource "coder_app" "windows-rdp" { } } +resource "coder_app" "native-rdp" { + count = var.enable_native_rdp ? 1 : 0 + + agent_id = var.agent_id + display_name = "RDP Desktop" + slug = "rdp-desktop" + icon = "/icon/rdp.svg" + external = true + order = var.order + group = var.group + tooltip = var.tooltip + url = "coder://${regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me[0].access_url)[0]}/v0/open/ws/${data.coder_workspace.me[0].name}/agent/${local.native_rdp_agent_name}/rdp?username=${urlencode(var.admin_username)}&password=${urlencode(var.admin_password)}" +} + resource "coder_app" "rdp-docs" { agent_id = var.agent_id display_name = "Local RDP Docs" diff --git a/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl new file mode 100644 index 000000000..3d107a4e1 --- /dev/null +++ b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl @@ -0,0 +1,82 @@ +mock_provider "coder" {} + +run "native_rdp_disabled" { + command = plan + + variables { + agent_id = "test-agent" + enable_native_rdp = false + } + + assert { + condition = length(coder_app.native-rdp) == 0 + error_message = "Disabling native RDP must omit the native app." + } + + assert { + condition = length(data.coder_workspace.me) == 0 + error_message = "The workspace data source must not be read when native RDP is disabled." + } +} + +run "native_rdp_requires_agent_name" { + command = plan + + variables { + agent_id = "test-agent" + } + + override_data { + target = data.coder_workspace.me + values = { + access_url = "https://coder.example.com" + name = "windows-workspace" + } + } + + expect_failures = [var.agent_name] +} + +run "native_rdp_enabled_by_default" { + command = plan + + variables { + agent_id = "test-agent" + agent_name = "windows-agent" + admin_username = "RDP User" + admin_password = "N;JVO*U\\mL^a*P\"'`$&<>|#%+" + } + + override_data { + target = data.coder_workspace.me + values = { + access_url = "https://coder.example.com" + name = "windows-workspace" + } + } + + assert { + condition = length(coder_app.native-rdp) == 1 + error_message = "Native RDP must create exactly one app by default." + } + + assert { + condition = coder_app.native-rdp[0].external + error_message = "The native RDP app must open through an external URI handler." + } + + assert { + condition = coder_app.native-rdp[0].icon == "/icon/rdp.svg" + error_message = "The native RDP app must use an icon distinct from Web RDP." + } + + assert { + condition = coder_app.native-rdp[0].tooltip == var.tooltip + error_message = "The native RDP app must explain its Coder Desktop dependency." + } + + assert { + condition = coder_app.native-rdp[0].url == "coder://coder.example.com/v0/open/ws/windows-workspace/agent/${var.agent_name}/rdp?username=${urlencode(var.admin_username)}&password=${urlencode(var.admin_password)}" + error_message = "The native RDP app must target the selected agent and URL-encode both credentials." + } +}