Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions registry/coder/modules/windows-rdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

Expand All @@ -30,21 +36,23 @@ 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"
}
```

### With Google Cloud

```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"
}
```

Expand All @@ -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"
}
```
15 changes: 12 additions & 3 deletions registry/coder/modules/windows-rdp/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}>;
Expand Down Expand Up @@ -67,13 +67,19 @@ function extractFormFieldValues(rdpScript: string): {
*/
describe("Web RDP", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(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<TestVariables>(import.meta.dir, {
agent_id: "foo",
enable_native_rdp: false,
});

const lines = findWindowsRdpScript(state)
Expand All @@ -99,6 +105,7 @@ describe("Web RDP", async () => {
import.meta.dir,
{
agent_id: "foo",
enable_native_rdp: false,
},
);

Expand All @@ -117,6 +124,7 @@ describe("Web RDP", async () => {
import.meta.dir,
{
agent_id: "foo",
enable_native_rdp: false,
admin_username: customAdminUsername,
admin_password: customAdminPassword,
},
Expand All @@ -138,6 +146,7 @@ describe("Web RDP", async () => {

const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
enable_native_rdp: false,
admin_password: specialPassword,
});

Expand Down
47 changes: 46 additions & 1 deletion registry/coder/modules/windows-rdp/main.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
terraform {
required_version = ">= 1.0"
required_version = ">= 1.9"

required_providers {
coder = {
Expand Down Expand Up @@ -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."
}
}
Comment thread
Edd88-pixel marked this conversation as resolved.

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
}
Comment thread
Edd88-pixel marked this conversation as resolved.

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"
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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)}"
}
Comment thread
Edd88-pixel marked this conversation as resolved.

resource "coder_app" "rdp-docs" {
agent_id = var.agent_id
display_name = "Local RDP Docs"
Expand Down
82 changes: 82 additions & 0 deletions registry/coder/modules/windows-rdp/windows-rdp.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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."
}
}