Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Brewer
e8f6578ece feat(jetbrains-gateway): bump version to 2024.1 (#220) 2024-04-11 02:36:25 +03:00
9 changed files with 52 additions and 1015 deletions

View File

@@ -1,5 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 6V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V6C1 4.35 2.35 3 4 3H28C29.65 3 31 4.35 31 6Z" fill="#2197F3"/>
<path d="M21 27H17V24C17 23.4478 16.5522 23 16 23C15.4478 23 15 23.4478 15 24V27H11C10.4478 27 10 27.4478 10 28C10 28.5522 10.4478 29 11 29H21C21.5522 29 22 28.5522 22 28C22 27.4478 21.5522 27 21 27Z" fill="#FFC10A"/>
<path d="M31 17V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V17H31Z" fill="#3F51B5"/>
</svg>

Before

Width:  |  Height:  |  Size: 540 B

View File

@@ -54,36 +54,36 @@ variable "jetbrains_ide_versions" {
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "233.14808.21"
version = "2023.3.5"
build_number = "241.14494.240"
version = "2024.1"
}
"PS" = {
build_number = "233.14808.18"
version = "2023.3.5"
build_number = "241.14494.237"
version = "2024.1"
}
"WS" = {
build_number = "233.14475.40"
version = "2023.3.4"
build_number = "241.14494.235"
version = "2024.1"
}
"PY" = {
build_number = "233.14475.56"
version = "2023.3.4"
build_number = "241.14494.241"
version = "2024.1"
}
"CL" = {
build_number = "233.14475.31"
version = "2023.3.4"
build_number = "241.14494.288"
version = "2024.1"
}
"GO" = {
build_number = "233.14808.20"
version = "2023.3.5"
build_number = "241.14494.238"
version = "2024.1"
}
"RM" = {
build_number = "233.14808.14"
version = "2023.3.5"
build_number = "241.14494.234"
version = "2024.1"
}
"RD" = {
build_number = "233.14475.66"
version = "2023.3.4"
build_number = "241.14494.307"
version = "2024.1"
}
}
validation {

263
package-lock.json generated
View File

@@ -1,263 +0,0 @@
{
"name": "modules",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "modules",
"devDependencies": {
"bun-types": "^1.0.18",
"gray-matter": "^4.0.3",
"marked": "^12.0.0",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.3.3"
}
},
"node_modules/@types/node": {
"version": "20.12.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
"integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/bun-types": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz",
"integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==",
"dev": true,
"dependencies": {
"@types/node": "~20.12.8",
"@types/ws": "~8.5.10"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mvdan-sh": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
"dev": true
},
"node_modules/prettier": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-sh": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
"dev": true,
"dependencies": {
"mvdan-sh": "^0.10.1",
"sh-syntax": "^0.4.1"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/prettier-plugin-terraform-formatter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
"dev": true,
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/sh-syntax": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
"dev": true,
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

73
test.ts
View File

@@ -29,10 +29,8 @@ export const runContainer = async (
return containerID.trim();
};
/**
* Finds the only "coder_script" resource in the given state and runs it in a
* container.
*/
// executeScriptInContainer finds the only "coder_script"
// resource in the given state and runs it in a container.
export const executeScriptInContainer = async (
state: TerraformState,
image: string,
@@ -78,22 +76,27 @@ export const execContainer = async (
};
};
type TerraformStateResource = {
type: string;
name: string;
provider: string;
instances: [{ attributes: Record<string, any> }];
};
export interface TerraformState {
outputs: {
[key: string]: {
type: string;
value: any;
};
};
resources: [TerraformStateResource, ...TerraformStateResource[]];
}
resources: [
{
type: string;
name: string;
provider: string;
instances: [
{
attributes: {
[key: string]: any;
};
},
];
},
];
}
export interface CoderScriptAttributes {
@@ -102,11 +105,10 @@ export interface CoderScriptAttributes {
url: 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>(
// 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>(
state: TerraformState,
type: T,
name?: string,
@@ -129,13 +131,12 @@ export const findResourceInstance = <T extends string>(
return resource.instances[0].attributes as any;
};
/**
* Creates a test-case for each variable provided and ensures that the apply
* fails without it.
*/
export const testRequiredVariables = <TVars extends Record<string, string>>(
// testRequiredVariables creates a test-case
// for each variable provided and ensures that
// the apply fails without it.
export const testRequiredVariables = (
dir: string,
vars: TVars,
vars: Record<string, string>,
) => {
// Ensures that all required variables are provided.
it("required variables", async () => {
@@ -164,16 +165,12 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
});
};
/**
* 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>>,
>(
// 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 (
dir: string,
vars: TVars,
vars: Record<string, string>,
): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
@@ -206,9 +203,7 @@ export const runTerraformApply = async <
return JSON.parse(content);
};
/**
* Runs terraform init in the given directory.
*/
// runTerraformInit runs terraform init in the given directory.
export const runTerraformInit = async (dir: string) => {
const proc = spawn(["terraform", "init"], {
cwd: dir,
@@ -226,5 +221,5 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
"Content-Type": "application/json",
},
status: statusCode,
});
};
})
}

View File

@@ -21,7 +21,7 @@ describe("vscode-desktop", async () => {
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
);
const resources = state.resources;
const resources: any = state.resources;
expect(resources[1].instances[0].attributes.order).toBeNull();
});
@@ -31,7 +31,7 @@ describe("vscode-desktop", async () => {
order: "22",
});
const resources = state.resources;
const resources: any = state.resources;
expect(resources[1].instances[0].attributes.order).toBe(22);
});
});

View File

@@ -1,35 +0,0 @@
---
display_name: Windows RDP
description: RDP Server and Web Client powered by Devolutions
icon: ../.icons/desktop.svg
maintainer_github: coder
verified: false
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)
[![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538)
## Usage
```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "github.com/coder/modules//windows-rdp?ref=web-rdp"
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
}
```
## Tested on
- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143)
## Roadmap
- [ ] Test on additional cloud providers
- [ ] Automatically establish web RDP session when users click "web RDP"
> This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality.

View File

@@ -1,410 +0,0 @@
// @ts-check
/**
* @file Defines the custom logic for patching in UI changes/behavior into the
* base Devolutions Gateway Angular app.
*
* Defined as a JS file to remove the need to have a separate compilation step.
* It is highly recommended that you work on this file from within VS Code so
* that you can take advantage of the @ts-check directive and get some type-
* checking still.
*
* Other notes about the weird ways this file is set up:
* - A lot of the HTML selectors in this file will look nonstandard. This is
* because they are actually custom Angular components.
* - It is strongly advised that you avoid template literals that use the
* placeholder syntax via the dollar sign. The Terraform script looks for
* these characters so that it can inject Coder-specific values, so any
* template literal that uses the character actually needs to double up each
* of them. There are already a few places in this file where it couldn't be
* avoided, but avoiding this as much as possible will save you some headache.
* - All the CSS should be written via custom style tags and the !important
* directive (as much as that is a bad idea most of the time). We do not
* control the Angular app, so we have to modify things from afar to ensure
* that as Angular's internal state changes, it doesn't modify its HTML nodes
* in a way that causes our custom styles to get wiped away.
*
* @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry
* @typedef {Readonly<Record<string, FormFieldEntry>>} FormFieldEntries
*/
/**
* The communication protocol to set Devolutions to.
*/
const PROTOCOL = "RDP";
/**
* The hostname to use with Devolutions.
*/
const HOSTNAME = "localhost";
/**
* How often to poll the screen for the main Devolutions form.
*/
const SCREEN_POLL_INTERVAL_MS = 500;
/**
* The fields in the Devolutions sign-in form that should be populated with
* values from the Coder workspace.
*
* All properties should be defined as placeholder templates in the form
* VALUE_NAME. The Coder module, when spun up, should then run some logic to
* replace the template slots with actual values. These values should never
* change from within JavaScript itself.
*
* @satisfies {FormFieldEntries}
*/
const formFieldEntries = {
/** @readonly */
username: {
/** @readonly */
querySelector: "web-client-username-control input",
/** @readonly */
value: "${CODER_USERNAME}",
},
/** @readonly */
password: {
/** @readonly */
querySelector: "web-client-password-control input",
/** @readonly */
value: "${CODER_PASSWORD}",
},
};
/**
* Handles typing in the values for the input form. All values are written
* immediately, even though that would be physically impossible with a real
* keyboard.
*
* Note: this code will never break, but you might get warnings in the console
* from Angular about unexpected value changes. Angular patches over a lot of
* the built-in browser APIs to support its component change detection system.
* As part of that, it has validations for checking whether an input it
* previously had control over changed without it doing anything.
*
* But the only way to simulate a keyboard input is by setting the input's
* .value property, and then firing an input event. So basically, the inner
* value will change, which Angular won't be happy about, but then the input
* event will fire and sync everything back together.
*
* @param {HTMLInputElement} inputField
* @param {string} inputText
* @returns {Promise<void>}
*/
function setInputValue(inputField, inputText) {
return new Promise((resolve, reject) => {
// Adding timeout for input event, even though we'll be dispatching it
// immediately, just in the off chance that something in the Angular app
// intercepts it or stops it from propagating properly
const timeoutId = window.setTimeout(() => {
reject(new Error("Input event did not get processed correctly in time."));
}, 3_000);
const handleSuccessfulDispatch = () => {
window.clearTimeout(timeoutId);
inputField.removeEventListener("input", handleSuccessfulDispatch);
resolve();
};
inputField.addEventListener("input", handleSuccessfulDispatch);
// Code assumes that Angular will have an event handler in place to handle
// the new event
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true,
});
inputField.value = inputText;
inputField.dispatchEvent(inputEvent);
});
}
/**
* Takes a Devolutions remote session form, auto-fills it with data, and then
* submits it.
*
* The logic here is more convoluted than it should be for two main reasons:
* 1. Devolutions' HTML markup has errors. There are labels, but they aren't
* bound to the inputs they're supposed to describe. This means no easy hooks
* for selecting the elements, unfortunately.
* 2. Trying to modify the .value properties on some of the inputs doesn't
* work. Probably some combo of Angular data-binding and some inputs having
* the readonly attribute. Have to simulate user input to get around this.
*
* @param {HTMLFormElement} myForm
* @returns {Promise<void>}
*/
async function autoSubmitForm(myForm) {
const setProtocolValue = () => {
/** @type {HTMLDivElement | null} */
const protocolDropdownTrigger = myForm.querySelector('div[role="button"]');
if (protocolDropdownTrigger === null) {
throw new Error("No clickable trigger for setting protocol value");
}
protocolDropdownTrigger.click();
// Can't use form as container for querying the list of dropdown options,
// because the elements don't actually exist inside the form. They're placed
// in the top level of the HTML doc, and repositioned to make it look like
// they're part of the form. Avoids CSS stacking context issues, maybe?
/** @type {HTMLLIElement | null} */
const protocolOption = document.querySelector(
'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li',
);
if (protocolOption === null) {
throw new Error(
"Unable to find protocol option on screen that matches desired protocol",
);
}
protocolOption.click();
};
const setHostname = () => {
/** @type {HTMLInputElement | null} */
const hostnameInput = myForm.querySelector("p-autocomplete#hostname input");
if (hostnameInput === null) {
throw new Error("Unable to find field for adding hostname");
}
return setInputValue(hostnameInput, HOSTNAME);
};
const setCoderFormFieldValues = async () => {
// The RDP form will not appear on screen unless the dropdown is set to use
// the RDP protocol
const rdpSubsection = myForm.querySelector("rdp-form");
if (rdpSubsection === null) {
throw new Error(
"Unable to find RDP subsection. Is the value of the protocol set to RDP?",
);
}
for (const { value, querySelector } of Object.values(formFieldEntries)) {
/** @type {HTMLInputElement | null} */
const input = document.querySelector(querySelector);
if (input === null) {
throw new Error(
'Unable to element that matches query "' + querySelector + '"',
);
}
await setInputValue(input, value);
}
};
const triggerSubmission = () => {
/** @type {HTMLButtonElement | null} */
const submitButton = myForm.querySelector(
'p-button[ng-reflect-type="submit"] button',
);
if (submitButton === null) {
throw new Error("Unable to find submission button");
}
if (submitButton.disabled) {
throw new Error(
"Unable to submit form because submit button is disabled. Are all fields filled out correctly?",
);
}
submitButton.click();
};
setProtocolValue();
await setHostname();
await setCoderFormFieldValues();
triggerSubmission();
}
/**
* Sets up logic for auto-populating the form data when the form appears on
* screen.
*
* @returns {void}
*/
function setupFormDetection() {
/** @type {HTMLFormElement | null} */
let formValueFromLastMutation = null;
/** @returns {void} */
const onDynamicTabMutation = () => {
/** @type {HTMLFormElement | null} */
const latestForm = document.querySelector("web-client-form > form");
// Only try to auto-fill if we went from having no form on screen to
// having a form on screen. That way, we don't accidentally override the
// form if the user is trying to customize values, and this essentially
// makes the script values function as default values
const mounted = formValueFromLastMutation === null && latestForm !== null;
if (mounted) {
autoSubmitForm(latestForm);
}
formValueFromLastMutation = latestForm;
};
/** @type {number | undefined} */
let pollingId = undefined;
/** @returns {void} */
const checkScreenForDynamicTab = () => {
const dynamicTab = document.querySelector("web-client-dynamic-tab");
// Keep polling until the main content container is on screen
if (dynamicTab === null) {
return;
}
window.clearInterval(pollingId);
// Call the mutation callback manually, to ensure it runs at least once
onDynamicTabMutation();
// Having the mutation observer is kind of an extra safety net that isn't
// really expected to run that often. Most of the content in the dynamic
// tab is being rendered through Canvas, which won't trigger any mutations
// that the observer can detect
const dynamicTabObserver = new MutationObserver(onDynamicTabMutation);
dynamicTabObserver.observe(dynamicTab, {
subtree: true,
childList: true,
});
};
pollingId = window.setInterval(
checkScreenForDynamicTab,
SCREEN_POLL_INTERVAL_MS,
);
}
/**
* Sets up custom styles for hiding default Devolutions elements that Coder
* users shouldn't need to care about.
*
* @returns {void}
*/
function setupAlwaysOnStyles() {
const styleId = "coder-patch--styles-always-on";
const existingContainer = document.querySelector("#" + styleId);
if (existingContainer) {
return;
}
const styleContainer = document.createElement("style");
styleContainer.id = styleId;
styleContainer.innerHTML = `
/* app-menu corresponds to the sidebar of the default view. */
app-menu {
display: none !important;
}
`;
document.head.appendChild(styleContainer);
}
function hideFormForInitialSubmission() {
const styleId = "coder-patch--styles-initial-submission";
const cssOpacityVariableName = "--coder-opacity-multiplier";
/** @type {HTMLStyleElement | null} */
let styleContainer = document.querySelector("#" + styleId);
if (!styleContainer) {
styleContainer = document.createElement("style");
styleContainer.id = styleId;
styleContainer.innerHTML = `
/*
Have to use opacity instead of visibility, because the element still
needs to be interactive via the script so that it can be auto-filled.
*/
:root {
/*
Can be 0 or 1. Start off invisible to avoid risks of UI flickering,
but the rest of the function should be in charge of making the form
container visible again if something goes wrong during setup.
Double dollar sign needed to avoid Terraform script false positives
*/
$${cssOpacityVariableName}: 0;
}
/*
web-client-form is the container for the main session form, while
the div is for the dropdown that is used for selecting the protocol.
The dropdown is not inside of the form for CSS styling reasons, so we
need to select both.
*/
web-client-form,
body > div.p-overlay {
/*
Double dollar sign needed to avoid Terraform script false positives
*/
opacity: calc(100% * var($${cssOpacityVariableName})) !important;
}
`;
document.head.appendChild(styleContainer);
}
// The root node being undefined should be physically impossible (if it's
// undefined, the browser itself is busted), but we need to do a type check
// here so that the rest of the function doesn't need to do type checks over
// and over.
const rootNode = document.querySelector(":root");
if (!(rootNode instanceof HTMLHtmlElement)) {
// Remove the container entirely because if the browser is busted, who knows
// if the CSS variables can be applied correctly. Better to have something
// be a bit more ugly/painful to use, than have it be impossible to use
styleContainer.remove();
return;
}
// It's safe to make the form visible preemptively because Devolutions
// outputs the Windows view through an HTML canvas that it overlays on top
// of the rest of the app. Even if the form isn't hidden at the style level,
// it will still be covered up.
const restoreOpacity = () => {
rootNode.style.setProperty(cssOpacityVariableName, "1");
};
// If this file gets more complicated, it might make sense to set up the
// timeout and event listener so that if one triggers, it cancels the other,
// but having restoreOpacity run more than once is a no-op for right now.
// Not a big deal if these don't get cleaned up.
// Have the form automatically reappear no matter what, so that if something
// does break, the user isn't left out to dry
window.setTimeout(restoreOpacity, 5_000);
/** @type {HTMLFormElement | null} */
const form = document.querySelector("web-client-form > form");
form?.addEventListener(
"submit",
() => {
// Not restoring opacity right away just to give the HTML canvas a little
// bit of time to get spun up and cover up the main form
window.setTimeout(restoreOpacity, 1_000);
},
{ once: true },
);
}
// Always safe to call these immediately because even if the Angular app isn't
// loaded by the time the function gets called, the CSS will always be globally
// available for when Angular is finally ready
setupAlwaysOnStyles();
hideFormForInitialSubmission();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", setupFormDetection);
} else {
setupFormDetection();
}

View File

@@ -1,72 +0,0 @@
import { describe, expect, it, test } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
type TestVariables = Readonly<{
agent_id: string;
resource_id: string;
admin_username?: string;
admin_password?: string;
}>;
describe("Web RDP", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
it("Installs the Devolutions Gateway Angular app locally on the machine", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
throw new Error("Not implemented yet");
});
/**
* @todo Verify that the HTML file has been modified, and that the JS file is
* also part of the file system
*/
it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
throw new Error("Not implemented yet");
});
it("Injects Terraform's username and password into the JS patch file", async () => {
throw new Error("Not implemented yet");
// Test that things work with the default username/password
const defaultState = await runTerraformApply<TestVariables>(
import.meta.dir,
{
agent_id: "foo",
resource_id: "bar",
},
);
const output = await executeScriptInContainer(defaultState, "alpine");
// Test that custom usernames/passwords are also forwarded correctly
const customUsername = "crouton";
const customPassword = "VeryVeryVeryVeryVerySecurePassword97!";
const customizedState = await runTerraformApply<TestVariables>(
import.meta.dir,
{
agent_id: "foo",
resource_id: "bar",
admin_username: customUsername,
admin_password: customPassword,
},
);
});
});

View File

@@ -1,173 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "resource_id" {
type = string
description = "The ID of the primary Coder resource (e.g. VM)."
}
variable "admin_username" {
type = string
default = "Administrator"
}
variable "admin_password" {
type = string
default = "coderRDP!"
sensitive = true
}
resource "coder_script" "windows-rdp" {
agent_id = var.agent_id
display_name = "windows-rdp"
icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons
script = <<EOF
function Set-AdminPassword {
param (
[string]$adminPassword
)
# Set admin password
Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
# Enable admin user
Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser
}
function Configure-RDP {
# Enable RDP
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -PropertyType DWORD -Force
# Disable NLA
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -PropertyType DWORD -Force
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
# Enable RDP through Windows Firewall
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
}
function Install-DevolutionsGateway {
# Define the module name and version
$moduleName = "DevolutionsGateway"
$moduleVersion = "2024.1.5"
Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force -Confirm:$false -SkipPublisherCheck
try {
# Try to import the module directly
Import-Module $moduleName -ErrorAction Stop
} catch {
# If it fails, install and then import the module
# Construct the module path for system-wide installation
$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion"
$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1"
# Import the module using the full path
Import-Module $modulePath
}
Install-DGatewayPackage
# Configure Devolutions Gateway
$Hostname = "localhost"
$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171'
$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None
$ConfigParams = @{
Hostname = $Hostname
Listeners = @($HttpListener)
WebApp = $WebApp
}
Set-DGatewayConfig @ConfigParams
New-DGatewayProvisionerKeyPair -Force
# Configure and start the Windows service
Set-Service 'DevolutionsGateway' -StartupType 'Automatic'
Start-Service 'DevolutionsGateway'
}
function Patch-Devolutions-HTML {
$root = "C:\Program Files\Devolutions\Gateway\webapp\client"
$devolutionsHtml = "$root\index.html"
$patch = '<script defer id="coder-patch" src="coder.js"></script>'
# Always copy the file in case we change it.
@'
${templatefile("${path.module}/devolutions-patch.js", {
CODER_USERNAME : var.admin_username,
CODER_PASSWORD : var.admin_password,
})}
'@ | Set-Content "$root\coder.js"
# Only inject the src if we have not before.
$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch
if ($isPatched -eq $null) {
(Get-Content $devolutionsHtml).Replace('</app-root>', "</app-root>$patch") | Set-Content $devolutionsHtml
}
}
Set-AdminPassword -adminPassword "${var.admin_password}"
Configure-RDP
Install-DevolutionsGateway
Patch-Devolutions-HTML
EOF
run_on_start = true
}
resource "coder_app" "windows-rdp" {
agent_id = var.agent_id
slug = "web-rdp"
display_name = "Web RDP"
url = "http://localhost:7171"
icon = "https://svgur.com/i/158F.svg"
subdomain = true
healthcheck {
url = "http://localhost:7171"
interval = 5
threshold = 15
}
}
resource "coder_app" "rdp-docs" {
agent_id = var.agent_id
display_name = "Local RDP"
slug = "rdp-docs"
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
url = "https://coder.com/docs/v2/latest/ides/remote-desktops#rdp-desktop"
external = true
}
# For some reason this is not rendering, commented out for now
# resource "coder_metadata" "rdp_details" {
# resource_id = var.resource_id
# daily_cost = 0
# item {
# key = "Host"
# value = "localhost"
# }
# item {
# key = "Port"
# value = "3389"
# }
# item {
# key = "Username"
# value = "Administrator"
# }
# item {
# key = "Password"
# value = var.admin_password
# sensitive = true
# }
# }