Skip to content

Commit

Permalink
Merge pull request #262 from coder/web-rdp
Browse files Browse the repository at this point in the history
feat: add module for Web RDP
  • Loading branch information
Parkreiner authored Jul 2, 2024
2 parents 45456ab + e8ee02c commit 8fd54e0
Show file tree
Hide file tree
Showing 9 changed files with 839 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .icons/desktop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 17 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 57 additions & 37 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export const runContainer = async (
return containerID.trim();
};

// executeScriptInContainer finds the only "coder_script"
// resource in the given state and runs it in a container.
/**
* Finds the only "coder_script" resource in the given state and runs it in a
* container.
*/
export const executeScriptInContainer = async (
state: TerraformState,
image: string,
Expand Down Expand Up @@ -76,27 +78,30 @@ export const execContainer = async (
};
};

type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };

type TerraformStateResource = {
type: string;
name: string;
provider: string;
instances: [{ attributes: Record<string, any> }];
};

export interface TerraformState {
outputs: {
[key: string]: {
type: string;
value: any;
};
}
resources: [
{
type: string;
name: string;
provider: string;
instances: [
{
attributes: {
[key: string]: any;
};
},
];
},
];
};

resources: [TerraformStateResource, ...TerraformStateResource[]];
}

export interface CoderScriptAttributes {
Expand All @@ -105,10 +110,11 @@ export interface CoderScriptAttributes {
url: string;
}

// findResourceInstance finds the first instance of the given resource
// type in the given state. If name is specified, it will only find
// the instance with the given name.
export const findResourceInstance = <T extends "coder_script" | string>(
/**
* finds the first instance of the given resource type in the given state. If
* name is specified, it will only find the instance with the given name.
*/
export const findResourceInstance = <T extends string>(
state: TerraformState,
type: T,
name?: string,
Expand All @@ -131,12 +137,13 @@ export const findResourceInstance = <T extends "coder_script" | string>(
return resource.instances[0].attributes as any;
};

// testRequiredVariables creates a test-case
// for each variable provided and ensures that
// the apply fails without it.
export const testRequiredVariables = (
/**
* Creates a test-case for each variable provided and ensures that the apply
* fails without it.
*/
export const testRequiredVariables = <TVars extends Record<string, string>>(
dir: string,
vars: Record<string, string>,
vars: TVars,
) => {
// Ensures that all required variables are provided.
it("required variables", async () => {
Expand Down Expand Up @@ -165,16 +172,25 @@ export const testRequiredVariables = (
});
};

// runTerraformApply runs terraform apply in the given directory
// with the given variables. It is fine to run in parallel with
// other instances of this function, as it uses a random state file.
export const runTerraformApply = async (
/**
* Runs terraform apply in the given directory with the given variables. It is
* fine to run in parallel with other instances of this function, as it uses a
* random state file.
*/
export const runTerraformApply = async <
TVars extends Readonly<Record<string, string | boolean>>,
>(
dir: string,
vars: Record<string, string>,
env: Record<string, string> = {},
vars: TVars,
env?: Record<string, string>,
): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));

const combinedEnv = env === undefined ? {} : { ...env };
for (const [key, value] of Object.entries(vars)) {
combinedEnv[`TF_VAR_${key}`] = String(value);
}

const proc = spawn(
[
"terraform",
Expand All @@ -188,22 +204,26 @@ export const runTerraformApply = async (
],
{
cwd: dir,
env,
env: combinedEnv,
stderr: "pipe",
stdout: "pipe",
},
);

const text = await readableStreamToText(proc.stderr);
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(text);
}

const content = await readFile(stateFile, "utf8");
await unlink(stateFile);
return JSON.parse(content);
};

// runTerraformInit runs terraform init in the given directory.
/**
* Runs terraform init in the given directory.
*/
export const runTerraformInit = async (dir: string) => {
const proc = spawn(["terraform", "init"], {
cwd: dir,
Expand All @@ -221,8 +241,8 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
"Content-Type": "application/json",
},
status: statusCode,
})
}
});
};

export const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
Expand Down
6 changes: 3 additions & 3 deletions vscode-desktop/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("vscode-desktop", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
open_recent: true,
open_recent: "true",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
Expand All @@ -54,7 +54,7 @@ describe("vscode-desktop", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
openRecent: false,
openRecent: "false",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
Expand All @@ -64,7 +64,7 @@ describe("vscode-desktop", async () => {
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
open_recent: true,
open_recent: "true",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
Expand Down
57 changes: 57 additions & 0 deletions windows-rdp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
display_name: Windows RDP
description: RDP Server and Web Client, powered by Devolutions Gateway
icon: ../.icons/desktop.svg
maintainer_github: coder
verified: true
tags: [windows, rdp, web, desktop]
---

# Windows RDP

Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway).

```tf
# AWS example. See below for examples of using this module with other providers
module "windows_rdp" {
source = "registry.coder.com/coder/module/windows-rdp"
version = "1.0.16"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
```

## Video

https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02

## Examples

### With AWS

```tf
module "windows_rdp" {
source = "registry.coder.com/coder/module/windows-rdp"
version = "1.0.16"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
```

### With Google Cloud

```tf
module "windows_rdp" {
source = "registry.coder.com/coder/module/windows-rdp"
version = "1.0.16"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
}
```

## Roadmap

- [ ] Test on Microsoft Azure.
Loading

0 comments on commit 8fd54e0

Please sign in to comment.