From 23ea3370d223b2ae503534aaaa05c4850a764e00 Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:16:34 +0100 Subject: [PATCH 1/3] feat(windows-rdp): add native client option --- registry/coder/modules/windows-rdp/README.md | 28 ++++++++-- registry/coder/modules/windows-rdp/main.tf | 29 ++++++++++ .../windows-rdp/windows-rdp.tftest.hcl | 54 +++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index bc5b4d3de..0a2e225a9 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -10,12 +10,16 @@ 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). +Set `enable_native_rdp = true` to add an **RDP Desktop** app that opens the +workspace in a native RDP client through [Coder Desktop](https://coder.com/docs/user-guides/desktop). +The native app uses the same administrator credentials as the web client. + ```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" + version = "1.4.0" agent_id = coder_agent.main.id } ``` @@ -32,7 +36,7 @@ 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 } ``` @@ -43,7 +47,7 @@ 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 } ``` @@ -54,8 +58,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 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 defaults to `main`; set `agent_name` when your template uses another name. + +```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" + enable_native_rdp = true +} +``` diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index c1035ce18..9f56c307d 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -53,6 +53,18 @@ 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." + type = string + default = "main" +} + +variable "enable_native_rdp" { + description = "Add an app button that opens the workspace in a native RDP client through Coder Desktop." + type = bool + default = false +} + variable "admin_username" { type = string default = "Administrator" @@ -85,6 +97,10 @@ locals { ps_admin_password = replace(var.admin_password, "'", "''") } +data "coder_workspace" "me" { + count = var.enable_native_rdp ? 1 : 0 +} + resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" @@ -125,6 +141,19 @@ 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/desktop.svg" + external = true + order = var.order + group = var.group + url = "coder://${regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me[0].access_url)[0]}/v0/open/ws/${data.coder_workspace.me[0].name}/agent/${var.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..eedebf2c3 --- /dev/null +++ b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl @@ -0,0 +1,54 @@ +mock_provider "coder" {} + +run "native_rdp_disabled_by_default" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = length(coder_app.native-rdp) == 0 + error_message = "The native RDP app must remain disabled by default." + } + + 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_enabled" { + command = plan + + variables { + agent_id = "test-agent" + agent_name = "windows-agent" + enable_native_rdp = true + 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 = "Enabling native RDP must create exactly one app." + } + + 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].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." + } +} From d75e3e81fdff3c8072938251ec7784f5b2dff763 Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:54:45 +0100 Subject: [PATCH 2/3] fix(windows-rdp): refine native client defaults --- registry/coder/modules/windows-rdp/README.md | 48 ++++++++++--------- .../coder/modules/windows-rdp/main.test.ts | 15 ++++-- registry/coder/modules/windows-rdp/main.tf | 28 +++++++++-- .../windows-rdp/windows-rdp.tftest.hcl | 48 +++++++++++++++---- 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index 0a2e225a9..0ea2c866b 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -10,17 +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). -Set `enable_native_rdp = true` to add an **RDP Desktop** app that opens the -workspace in a native RDP client through [Coder Desktop](https://coder.com/docs/user-guides/desktop). -The native app uses the same administrator credentials as the web client. +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.4.0" - 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" } ``` @@ -34,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.4.0" - 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" } ``` @@ -45,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.4.0" - 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" } ``` @@ -60,6 +64,7 @@ module "windows_rdp" { source = "registry.coder.com/coder/windows-rdp/coder" version = "1.4.0" agent_id = coder_agent.main.id + agent_name = "main" devolutions_gateway_version = "2025.2.2" # Specify a specific version } ``` @@ -67,15 +72,14 @@ module "windows_rdp" { ### With Native RDP Coder Desktop must be installed and connected on the local device. The agent -name defaults to `main`; set `agent_name` when your template uses another name. +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" - enable_native_rdp = true + 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 9f56c307d..63c0688fd 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -54,15 +54,21 @@ variable "agent_id" { } variable "agent_name" { - description = "The name of the Coder agent used for native RDP connections." + description = "The name of the Coder agent used for native RDP connections. Required when enable_native_rdp is true." type = string - default = "main" + default = null } variable "enable_native_rdp" { description = "Add an app button that opens the workspace in a native RDP client through Coder Desktop." type = bool - default = false + 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" { @@ -95,6 +101,10 @@ 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. The + # precondition below rejects this placeholder whenever native RDP is enabled. + native_rdp_agent_name = var.agent_name != null ? var.agent_name : "" } data "coder_workspace" "me" { @@ -147,11 +157,19 @@ resource "coder_app" "native-rdp" { agent_id = var.agent_id display_name = "RDP Desktop" slug = "rdp-desktop" - icon = "/icon/desktop.svg" + icon = "/icon/rdp.svg" external = true order = var.order group = var.group - url = "coder://${regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me[0].access_url)[0]}/v0/open/ws/${data.coder_workspace.me[0].name}/agent/${var.agent_name}/rdp?username=${urlencode(var.admin_username)}&password=${urlencode(var.admin_password)}" + 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)}" + + lifecycle { + precondition { + condition = var.agent_name != null + error_message = "agent_name must be set when enable_native_rdp is true." + } + } } resource "coder_app" "rdp-docs" { diff --git a/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl index eedebf2c3..0ce6a4287 100644 --- a/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl +++ b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl @@ -1,15 +1,16 @@ mock_provider "coder" {} -run "native_rdp_disabled_by_default" { +run "native_rdp_disabled" { command = plan variables { - agent_id = "test-agent" + agent_id = "test-agent" + enable_native_rdp = false } assert { condition = length(coder_app.native-rdp) == 0 - error_message = "The native RDP app must remain disabled by default." + error_message = "Disabling native RDP must omit the native app." } assert { @@ -18,15 +19,32 @@ run "native_rdp_disabled_by_default" { } } -run "native_rdp_enabled" { +run "native_rdp_requires_agent_name" { command = plan variables { - agent_id = "test-agent" - agent_name = "windows-agent" - enable_native_rdp = true - admin_username = "RDP User" - admin_password = "N;JVO*U\\mL^a*P\"'`$&<>|#%+" + agent_id = "test-agent" + } + + override_data { + target = data.coder_workspace.me + values = { + access_url = "https://coder.example.com" + name = "windows-workspace" + } + } + + expect_failures = [coder_app.native-rdp] +} + +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 { @@ -39,7 +57,7 @@ run "native_rdp_enabled" { assert { condition = length(coder_app.native-rdp) == 1 - error_message = "Enabling native RDP must create exactly one app." + error_message = "Native RDP must create exactly one app by default." } assert { @@ -47,6 +65,16 @@ run "native_rdp_enabled" { 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." From 93cc773e7c57aab2377935f246304a95ba077dca Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:13:13 +0100 Subject: [PATCH 3/3] fix(windows-rdp): validate native agent input --- registry/coder/modules/windows-rdp/main.tf | 18 ++++++++---------- .../modules/windows-rdp/windows-rdp.tftest.hcl | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index 63c0688fd..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 = { @@ -57,6 +57,11 @@ 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" { @@ -102,8 +107,8 @@ locals { ps_admin_username = replace(var.admin_username, "'", "''") ps_admin_password = replace(var.admin_password, "'", "''") - # Terraform still evaluates the disabled resource configuration. The - # precondition below rejects this placeholder whenever native RDP is enabled. + # 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 : "" } @@ -163,13 +168,6 @@ resource "coder_app" "native-rdp" { 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)}" - - lifecycle { - precondition { - condition = var.agent_name != null - error_message = "agent_name must be set when enable_native_rdp is true." - } - } } resource "coder_app" "rdp-docs" { diff --git a/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl index 0ce6a4287..3d107a4e1 100644 --- a/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl +++ b/registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl @@ -34,7 +34,7 @@ run "native_rdp_requires_agent_name" { } } - expect_failures = [coder_app.native-rdp] + expect_failures = [var.agent_name] } run "native_rdp_enabled_by_default" {