From c5c521fabdf5739bea819d4d47781584f8e7ea6c Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 19:48:37 +0000 Subject: [PATCH 01/96] feat: add web RDP module --- .icons/desktop.svg | 5 +++ windows-rdp/README.md | 28 +++++++++++++ windows-rdp/main.tf | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 .icons/desktop.svg create mode 100644 windows-rdp/README.md create mode 100644 windows-rdp/main.tf diff --git a/.icons/desktop.svg b/.icons/desktop.svg new file mode 100644 index 0000000..77d231c --- /dev/null +++ b/.icons/desktop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows-rdp/README.md b/windows-rdp/README.md new file mode 100644 index 0000000..ecede44 --- /dev/null +++ b/windows-rdp/README.md @@ -0,0 +1,28 @@ +--- +display_name: Windows RDP +description: RDP Server and Web Client powered by Devolutions +icon: ../.icons/desktop.svg +maintainer_github: coder +verified: false +tags: [windows, ide, web] +--- + +# Windows RDP + +Enable Remote Desktop + a web based client on Windows workspaces + + + +## Usage + +```tf +module "code-server" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.0.10" + agent_id = coder_agent.example.id +} +``` + +## Tested on + +- ✅ GCP with Windows Server 2022: [Example template](#TODO) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf new file mode 100644 index 0000000..81ac95f --- /dev/null +++ b/windows-rdp/main.tf @@ -0,0 +1,98 @@ +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." +} + +resource "coder_script" "windows-rdp" { + agent_id = var.agent_id + display_name = "web-rdp" + icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons + script = < Date: Sat, 6 Apr 2024 19:51:51 +0000 Subject: [PATCH 02/96] fix port typo --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 81ac95f..0e7d679 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -91,7 +91,7 @@ resource "coder_app" "windows-rdp" { subdomain = true healthcheck { - url = "http://localhost:${var.port}" + url = "http://localhost:7171" interval = 5 threshold = 15 } From 12fd16f701197ad718274791a9d26bd8ad0dbbb1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:01:57 +0000 Subject: [PATCH 03/96] add metadata and local instructions --- windows-rdp/main.tf | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 0e7d679..1ca1372 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -14,6 +14,17 @@ variable "agent_id" { 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_password" { + type = string + default = "coderRDP!" + sensitive = true +} + resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "web-rdp" @@ -73,7 +84,7 @@ resource "coder_script" "windows-rdp" { Start-Service 'DevolutionsGateway' } - Set-AdminPassword -adminPassword "coderRDP!" + Set-AdminPassword -adminPassword "${var.admin_password}" Configure-RDP Install-DevolutionsGateway @@ -96,3 +107,35 @@ resource "coder_app" "windows-rdp" { threshold = 15 } } + +resource "coder_app" "rdp-docs" { + agent_id = coder_agent.main.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 +} + +resource "coder_metadata" "rdp_details" { + count = data.coder_workspace.me.start_count + 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 + } +} From bf06e8d3ac0f4b988e37ec63af65f03e58db0488 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:04:28 +0000 Subject: [PATCH 04/96] fix agent id --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1ca1372..4211059 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -109,7 +109,7 @@ resource "coder_app" "windows-rdp" { } resource "coder_app" "rdp-docs" { - agent_id = coder_agent.main.id + agent_id = var.agent_id display_name = "Local RDP" slug = "rdp-docs" icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" From 0e7644b284d6ef809add791714f1f3639516547e Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:05:57 +0000 Subject: [PATCH 05/96] remove count --- windows-rdp/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 4211059..4b42050 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -118,7 +118,6 @@ resource "coder_app" "rdp-docs" { } resource "coder_metadata" "rdp_details" { - count = data.coder_workspace.me.start_count resource_id = var.resource_id daily_cost = 0 item { From 9f8eee55b2c3b612737421d8c96048baa175c750 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:11:59 +0000 Subject: [PATCH 06/96] rename script --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 4b42050..fd086f0 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -27,7 +27,7 @@ variable "admin_password" { resource "coder_script" "windows-rdp" { agent_id = var.agent_id - display_name = "web-rdp" + display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons script = < Date: Sat, 6 Apr 2024 20:13:50 +0000 Subject: [PATCH 07/96] remove metadata for now --- windows-rdp/main.tf | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index fd086f0..5507018 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -117,24 +117,24 @@ resource "coder_app" "rdp-docs" { external = true } -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 - } -} +# 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 +# } +# } From 748a180ac3ac7e84be052052a97c759c456a8a1d Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:18:58 +0000 Subject: [PATCH 08/96] add temp link to example template --- windows-rdp/README.md | 2 +- windows-rdp/main.tf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index ecede44..71a4873 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -25,4 +25,4 @@ module "code-server" { ## Tested on -- ✅ GCP with Windows Server 2022: [Example template](#TODO) +- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 5507018..1b557eb 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -117,6 +117,7 @@ resource "coder_app" "rdp-docs" { 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 From ac648cc0a90bcb7e4413b357f96bff3fb1c10fe0 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:32:52 +0000 Subject: [PATCH 09/96] add thumbnail --- windows-rdp/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 71a4873..d7b016c 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -11,7 +11,7 @@ tags: [windows, ide, web] Enable Remote Desktop + a web based client on Windows workspaces - +[![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538) ## Usage @@ -26,3 +26,9 @@ module "code-server" { ## 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 + > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp) \ No newline at end of file From 89135671b26f1f5e7cb28feead37ebcf60fd72a3 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:34:06 +0000 Subject: [PATCH 10/96] fix module usage --- windows-rdp/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index d7b016c..1529084 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -16,10 +16,11 @@ Enable Remote Desktop + a web based client on Windows workspaces ## Usage ```tf -module "code-server" { - source = "registry.coder.com/modules/code-server/coder" - version = "1.0.10" - agent_id = coder_agent.example.id +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 } ``` From 7de78d2ef5392adc809666ca590748c905ed151e Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:36:55 +0000 Subject: [PATCH 11/96] add tags --- windows-rdp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 1529084..32ec091 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -4,12 +4,12 @@ description: RDP Server and Web Client powered by Devolutions icon: ../.icons/desktop.svg maintainer_github: coder verified: false -tags: [windows, ide, web] +tags: [windows, rdp, web, desktop] --- # Windows RDP -Enable Remote Desktop + a web based client on Windows workspaces +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) From 53083a5718c557e80021ac28e53d41b699ea7cd1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 6 Apr 2024 20:46:50 +0000 Subject: [PATCH 12/96] add more context on auto login --- windows-rdp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 32ec091..6320ca6 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -31,5 +31,5 @@ module "windows_rdp" { ## Roadmap - [ ] Test on additional cloud providers -- [ ] Automatically establish web RDP session - > This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp) \ No newline at end of file +- [ ] 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. \ No newline at end of file From b93471a3814a7be91b43d86c22b9ad266fe5e0ab Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 24 Apr 2024 22:39:24 +0000 Subject: [PATCH 13/96] chore: add admin username --- windows-rdp/main.tf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1b557eb..9e53521 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -19,6 +19,11 @@ variable "resource_id" { 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!" @@ -35,9 +40,9 @@ resource "coder_script" "windows-rdp" { [string]$adminPassword ) # Set admin password - Get-LocalUser -Name "Administrator" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) + Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user - Get-LocalUser -Name "Administrator" | Enable-LocalUser + Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser } function Configure-RDP { From 20795aa2b642ecf26db634e5c9cfd823e10e43aa Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 19:37:31 +0000 Subject: [PATCH 14/96] chore: add script file for overriding Devolutions --- windows-rdp/devolutions-patch.js | 361 +++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 windows-rdp/devolutions-patch.js diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js new file mode 100644 index 0000000..00260d2 --- /dev/null +++ b/windows-rdp/devolutions-patch.js @@ -0,0 +1,361 @@ +// @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. + * + * A lot of the HTML selectors in this file will look nonstandard. This is + * because they are actually custom Angular components. + * + * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry + * @typedef {Readonly>} 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, dispatching each character + * as an event. This function assumes that all characters in the input will be + * UTF-8. + * + * 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} + */ +function setInputValue(inputField, inputText) { + const continueEventName = "coder-patch--continue"; + + const promise = /** @type {Promise} */ ( + new Promise((resolve, reject) => { + if (inputText === "") { + resolve(); + return; + } + + // -1 indicates a "pre-write" for clearing out the input before trying to + // write new text to it + let i = -1; + + // requestAnimationFrame is not capable of giving back values of 0 for its + // task IDs. Good default value to ensure that we don't need if statements + // when trying to cancel anything + let currentAnimationId = 0; + + // Super easy to pool the same event objects, because the events don't + // have any custom, context-specific values on them, and they're + // restricted to this one callback. + const continueEvent = new CustomEvent(continueEventName); + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + /** @returns {void} */ + const handleNextCharIndex = () => { + if (i === inputText.length) { + resolve(); + return; + } + + const currentChar = inputText[i]; + if (i !== -1 && currentChar === undefined) { + throw new Error("Went out of bounds"); + } + + try { + inputField.addEventListener( + continueEventName, + () => { + i++; + currentAnimationId = + window.requestAnimationFrame(handleNextCharIndex); + }, + { once: true }, + ); + + if (i === -1) { + inputField.value = ""; + } else { + inputField.value = `${inputField.value}${currentChar}`; + } + + inputField.dispatchEvent(inputEvent); + inputField.dispatchEvent(continueEvent); + } catch (err) { + cancelAnimationFrame(currentAnimationId); + reject(err); + } + }; + + currentAnimationId = window.requestAnimationFrame(handleNextCharIndex); + }) + ); + + return promise; +} + +/** + * 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} + */ +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 = () => { + console.log("Ran on mutation!"); + + /** @type {HTMLFormElement | null} */ + const latestForm = document.querySelector("web-client-form > form"); + + if (latestForm === null) { + formValueFromLastMutation = null; + return; + } + + // 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 + if (formValueFromLastMutation === null) { + 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 setupObscuringStyles() { + const styleId = "coder-patch--styles"; + + 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); +} + +// Always safe to call setupObscuringStyles 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 +setupObscuringStyles(); + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupFormDetection); +} else { + setupFormDetection(); +} From ff96b3f65397f327a83321b3fe1c5b79a886a347 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:07:39 +0000 Subject: [PATCH 15/96] wip: commit current progress for devolutions patch --- package-lock.json | 263 +++++++++++++++++++++++++++++++ windows-rdp/devolutions-patch.js | 8 +- windows-rdp/main.tf | 16 ++ 3 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4828ced --- /dev/null +++ b/package-lock.json @@ -0,0 +1,263 @@ +{ + "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 + } + } +} diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 00260d2..cf64b42 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -35,8 +35,8 @@ const SCREEN_POLL_INTERVAL_MS = 500; * 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 + * 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} @@ -48,7 +48,7 @@ const formFieldEntries = { querySelector: "web-client-username-control input", /** @readonly */ - value: "{{ CODER_USERNAME }}", + value: "CODER_USERNAME", }, /** @readonly */ @@ -57,7 +57,7 @@ const formFieldEntries = { querySelector: "web-client-password-control input", /** @readonly */ - value: "{{ CODER_PASSWORD }}", + value: "CODER_PASSWORD", }, }; diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9e53521..de3d408 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -89,9 +89,25 @@ resource "coder_script" "windows-rdp" { Start-Service 'DevolutionsGateway' } + function Patch-Devolutions-HTML { + $root = "C:\Program Files\Devolutions\Gateway\webapp\client" + $devolutionsHtml = "$root\index.html" + $patch = '' + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + if ($isPatched -e $null) { + "templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, + }" | Set-Content "$root\coder.js" + + (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml + } + } + Set-AdminPassword -adminPassword "${var.admin_password}" Configure-RDP Install-DevolutionsGateway + Patch-Devolutions-HTML EOF From aab5e55663f951c1f0c1f27119bd6c4db582b80d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:10:22 +0000 Subject: [PATCH 16/96] fix: update script frequency --- windows-rdp/main.tf | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index de3d408..99de193 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -94,12 +94,13 @@ resource "coder_script" "windows-rdp" { $devolutionsHtml = "$root\index.html" $patch = '' $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + # 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. if ($isPatched -e $null) { - "templatefile("${path.module}/devolutions-patch.js", { - CODER_USERNAME : var.admin_username, - CODER_PASSWORD : var.admin_password, - }" | Set-Content "$root\coder.js" - (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } } From 29209d546edc24346dba1200cd675b7c874cb96b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:13:11 +0000 Subject: [PATCH 17/96] fix: update typo in powershell script Co-authored-by: Asher --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 99de193..30f1a1e 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -100,7 +100,7 @@ resource "coder_script" "windows-rdp" { CODER_PASSWORD : var.admin_password, }" | Set-Content "$root\coder.js" # Only inject the src if we have not before. - if ($isPatched -e $null) { + if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } } From 452f41aa86d498eae90fb72b6bd71cc586d203f6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:17:31 +0000 Subject: [PATCH 18/96] fix: add parenthesis --- windows-rdp/main.tf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 30f1a1e..a098669 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -93,13 +93,15 @@ resource "coder_script" "windows-rdp" { $root = "C:\Program Files\Devolutions\Gateway\webapp\client" $devolutionsHtml = "$root\index.html" $patch = '' - $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + # Always copy the file in case we change it. - "templatefile("${path.module}/devolutions-patch.js", { + "${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }" | Set-Content "$root\coder.js" + })}" | Set-Content "$root\coder.js" + # Only inject the src if we have not before. + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } From c7aa8253e3a366401a60eaadf51fdd0efc3c4fec Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:22:25 +0000 Subject: [PATCH 19/96] fix: dolla dolla --- windows-rdp/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a098669..594d79f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,10 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${templatefile("${path.module}/devolutions-patch.js", { + "${replace(templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - })}" | Set-Content "$root\coder.js" + }), "$", "\\$")}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 047ccd67ca9f57c03b1d0cf98ea41f4ff6645ea6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:24:49 +0000 Subject: [PATCH 20/96] fix: dolla dolla --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 594d79f..a489768 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -98,7 +98,7 @@ resource "coder_script" "windows-rdp" { "${replace(templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }), "$", "\\$")}" | Set-Content "$root\coder.js" + }), "$", "$$")}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From d530d68b12a5ca33779e8cbf666ac972b25fbce6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:28:44 +0000 Subject: [PATCH 21/96] fix: more money, more problems --- windows-rdp/devolutions-patch.js | 12 ++++++------ windows-rdp/main.tf | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index cf64b42..ecb16c1 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -48,7 +48,7 @@ const formFieldEntries = { querySelector: "web-client-username-control input", /** @readonly */ - value: "CODER_USERNAME", + value: "${CODER_USERNAME}", }, /** @readonly */ @@ -57,7 +57,7 @@ const formFieldEntries = { querySelector: "web-client-password-control input", /** @readonly */ - value: "CODER_PASSWORD", + value: "${CODER_PASSWORD}", }, }; @@ -135,7 +135,7 @@ function setInputValue(inputField, inputText) { if (i === -1) { inputField.value = ""; } else { - inputField.value = `${inputField.value}${currentChar}`; + inputField.value = `$${inputField.value}$${currentChar}`; } inputField.dispatchEvent(inputEvent); @@ -184,7 +184,7 @@ async function autoSubmitForm(myForm) { // 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`, + `p-dropdownitem[ng-reflect-label="$${PROTOCOL}"] li`, ); if (protocolOption === null) { @@ -223,7 +223,7 @@ async function autoSubmitForm(myForm) { if (input === null) { throw new Error( - `Unable to element that matches query "${querySelector}"`, + `Unable to element that matches query "$${querySelector}"`, ); } @@ -332,7 +332,7 @@ function setupFormDetection() { function setupObscuringStyles() { const styleId = "coder-patch--styles"; - const existingContainer = document.querySelector(`#${styleId}`); + const existingContainer = document.querySelector(`#$${styleId}`); if (existingContainer) { return; } diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a489768..a098669 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,10 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${replace(templatefile("${path.module}/devolutions-patch.js", { + "${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - }), "$", "$$")}" | Set-Content "$root\coder.js" + })}" | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 0b6975c266c3f280576b354d46bf439f2bb273ed Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 20:41:45 +0000 Subject: [PATCH 22/96] fix: escape quotes --- windows-rdp/main.tf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index a098669..dafae70 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -95,10 +95,12 @@ resource "coder_script" "windows-rdp" { $patch = '' # Always copy the file in case we change it. - "${templatefile("${path.module}/devolutions-patch.js", { + @' + ${templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, - })}" | Set-Content "$root\coder.js" + })} + '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" From 14e3fc5b6bedb71ea9d87876f91d9b3f227de279 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 21:13:15 +0000 Subject: [PATCH 23/96] fix: whitespace --- windows-rdp/main.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index dafae70..7d95ec2 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -96,11 +96,11 @@ resource "coder_script" "windows-rdp" { # 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" +${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" From fba0f842a9e7c4badbff6a9d3f1e21c85d428978 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 24 Jun 2024 21:47:01 +0000 Subject: [PATCH 24/96] fix: remove regex search from Select-String --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 7d95ec2..9e1d8fb 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -103,7 +103,7 @@ ${templatefile("${path.module}/devolutions-patch.js", { '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. - $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" + $isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch if ($isPatched -eq $null) { (Get-Content $devolutionsHtml).Replace('', "$patch") | Set-Content $devolutionsHtml } From d5cfadb4e7dccb8fc32f9fa652e95444f4e0b5aa Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 17:03:54 +0000 Subject: [PATCH 25/96] fix: remove template literal dollar signs --- windows-rdp/devolutions-patch.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index ecb16c1..cd1284f 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -8,8 +8,14 @@ * that you can take advantage of the @ts-check directive and get some type- * checking still. * - * A lot of the HTML selectors in this file will look nonstandard. This is - * because they are actually custom Angular components. + * 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 * * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry * @typedef {Readonly>} FormFieldEntries @@ -135,7 +141,7 @@ function setInputValue(inputField, inputText) { if (i === -1) { inputField.value = ""; } else { - inputField.value = `$${inputField.value}$${currentChar}`; + inputField.value = inputField.value + currentChar; } inputField.dispatchEvent(inputEvent); @@ -171,7 +177,7 @@ function setInputValue(inputField, inputText) { async function autoSubmitForm(myForm) { const setProtocolValue = () => { /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector(`div[role="button"]`); + const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); if (protocolDropdownTrigger === null) { throw new Error("No clickable trigger for setting protocol value"); } @@ -184,7 +190,7 @@ async function autoSubmitForm(myForm) { // 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`, + 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '" li', ); if (protocolOption === null) { @@ -223,7 +229,7 @@ async function autoSubmitForm(myForm) { if (input === null) { throw new Error( - `Unable to element that matches query "$${querySelector}"`, + 'Unable to element that matches query "' + querySelector + '"', ); } @@ -332,7 +338,7 @@ function setupFormDetection() { function setupObscuringStyles() { const styleId = "coder-patch--styles"; - const existingContainer = document.querySelector(`#$${styleId}`); + const existingContainer = document.querySelector("#" + styleId); if (existingContainer) { return; } From 8195cf445381044b92f9039262712ce044412f49 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:48:44 +0000 Subject: [PATCH 26/96] wip: add current code for hiding Devolutions form --- windows-rdp/devolutions-patch.js | 113 ++++++++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index cd1284f..aa001ac 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -16,6 +16,11 @@ * 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 + * - 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>} FormFieldEntries @@ -90,7 +95,7 @@ const formFieldEntries = { function setInputValue(inputField, inputText) { const continueEventName = "coder-patch--continue"; - const promise = /** @type {Promise} */ ( + const keyboardInputPromise = /** @type {Promise} */ ( new Promise((resolve, reject) => { if (inputText === "") { resolve(); @@ -156,7 +161,7 @@ function setInputValue(inputField, inputText) { }) ); - return promise; + return keyboardInputPromise; } /** @@ -274,8 +279,6 @@ function setupFormDetection() { /** @returns {void} */ const onDynamicTabMutation = () => { - console.log("Ran on mutation!"); - /** @type {HTMLFormElement | null} */ const latestForm = document.querySelector("web-client-form > form"); @@ -335,9 +338,8 @@ function setupFormDetection() { * * @returns {void} */ -function setupObscuringStyles() { - const styleId = "coder-patch--styles"; - +function setupAlwaysOnStyles() { + const styleId = "coder-patch--styles-always-on"; const existingContainer = document.querySelector("#" + styleId); if (existingContainer) { return; @@ -355,10 +357,105 @@ function setupObscuringStyles() { document.head.appendChild(styleContainer); } +function hideFormForInitialSubmission() { + const styleId = "coder-patch--styles-initial-submission"; + const existingContainer = document.querySelector("#" + styleId); + if (existingContainer) { + return; + } + + const 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. + */ + --coder-opacity-multiplier: 1; + } + + /* web-client-form is the container for the main session form */ + web-client-form { + opacity: calc(100% * var(--coder-opacity-multiplier)) !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 HTMLElement)) { + styleContainer.innerHTML = ""; + return; + } + + /** @type {number | undefined} */ + let intervalId = undefined; + const maxScreenPolls = 3; + let pollAttempts = 0; + + const checkIfSafeToHideForm = () => { + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + if (form === null) { + pollAttempts++; + if (pollAttempts === maxScreenPolls) { + window.clearInterval(intervalId); + } + + return; + } + + // Now that we know the container exists, it's safe to hide it + rootNode.style.setProperty("--coder-opacity-multiplier", "0"); + + // 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("--coder-opacity-multiplier", "1"); + }; + + const timeoutId = window.setTimeout(() => { + restoreOpacity(); + form.removeEventListener("submit", restoreOpacity); + }, 5_000); + + form.addEventListener( + "submit", + () => { + restoreOpacity(); + window.clearTimeout(timeoutId); + }, + { once: true }, + ); + }; + + intervalId = window.setInterval( + checkIfSafeToHideForm, + SCREEN_POLL_INTERVAL_MS, + ); +} + +function setupFormOverrides() { + hideFormForInitialSubmission(); + setupFormDetection(); +} + // Always safe to call setupObscuringStyles 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 -setupObscuringStyles(); +setupAlwaysOnStyles(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", setupFormDetection); From 652fc6b84fcc73a846a5755822979448338bd40b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:55:14 +0000 Subject: [PATCH 27/96] refactor: clean up form code --- windows-rdp/devolutions-patch.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index aa001ac..582f791 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -426,19 +426,12 @@ function hideFormForInitialSubmission() { rootNode.style.setProperty("--coder-opacity-multiplier", "1"); }; - const timeoutId = window.setTimeout(() => { - restoreOpacity(); - form.removeEventListener("submit", restoreOpacity); - }, 5_000); - - form.addEventListener( - "submit", - () => { - restoreOpacity(); - window.clearTimeout(timeoutId); - }, - { once: true }, - ); + // 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. + window.setTimeout(restoreOpacity, 5_000); + form.addEventListener("submit", restoreOpacity, { once: true }); }; intervalId = window.setInterval( From 702271133f3739d1421470890c3fdbb808148789 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:57:48 +0000 Subject: [PATCH 28/96] fix: update HTML query selector --- windows-rdp/devolutions-patch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 582f791..d5e1e25 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -195,7 +195,7 @@ async function autoSubmitForm(myForm) { // 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', + 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', ); if (protocolOption === null) { @@ -451,7 +451,7 @@ function setupFormOverrides() { setupAlwaysOnStyles(); if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); + document.addEventListener("DOMContentLoaded", setupFormOverrides); } else { - setupFormDetection(); + setupFormOverrides(); } From 5ec1b207d13d2f2cf20a52e71154ac7d13e61856 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 19:58:56 +0000 Subject: [PATCH 29/96] docs: remove now-inaccurate comment --- windows-rdp/devolutions-patch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index d5e1e25..f7673ed 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -445,9 +445,9 @@ function setupFormOverrides() { setupFormDetection(); } -// Always safe to call setupObscuringStyles 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 +// Always safe to call this 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(); if (document.readyState === "loading") { From c7a4fced4c1ef91b8952b2c980225de79c5d9f05 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:15:18 +0000 Subject: [PATCH 30/96] fix: update instanceof check --- windows-rdp/devolutions-patch.js | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index f7673ed..b4d49d1 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -359,41 +359,41 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; - } - const 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 { + /** @type {HTMLStyleElement | null} */ + let styleContainer = document.querySelector("#" + styleId); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` /* - 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. + 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. */ - --coder-opacity-multiplier: 1; - } + :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. + */ + --coder-opacity-multiplier: 1; + } - /* web-client-form is the container for the main session form */ - web-client-form { - opacity: calc(100% * var(--coder-opacity-multiplier)) !important; - } - `; + /* web-client-form is the container for the main session form */ + web-client-form { + opacity: calc(100% * var(--coder-opacity-multiplier)) !important; + } + `; - document.head.appendChild(styleContainer); + 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 HTMLElement)) { + if (!(rootNode instanceof HTMLHtmlElement)) { styleContainer.innerHTML = ""; return; } From 1a0a8659ccd43b50bef5d5d9e588dbec52de6366 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:40:44 +0000 Subject: [PATCH 31/96] wip: update logic for hiding form to avoid whiffs --- windows-rdp/devolutions-patch.js | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index b4d49d1..fd97cfb 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -15,7 +15,8 @@ * 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 + * of them. There are already a few places in this file where it couldn't be + * avoided, but it 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 @@ -359,6 +360,7 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; + const opacityVariableName = "--coder-opacity-multiplier"; /** @type {HTMLStyleElement | null} */ let styleContainer = document.querySelector("#" + styleId); @@ -376,12 +378,12 @@ function hideFormForInitialSubmission() { but the rest of the function should be in charge of making the form container visible again if something goes wrong during setup. */ - --coder-opacity-multiplier: 1; + $${opacityVariableName}: 0; } /* web-client-form is the container for the main session form */ web-client-form { - opacity: calc(100% * var(--coder-opacity-multiplier)) !important; + opacity: calc(100% * var($${opacityVariableName})) !important; } `; @@ -398,9 +400,17 @@ function hideFormForInitialSubmission() { 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(opacityVariableName, "1"); + }; + /** @type {number | undefined} */ let intervalId = undefined; - const maxScreenPolls = 3; + const pollingTimeoutMs = 5_000; let pollAttempts = 0; const checkIfSafeToHideForm = () => { @@ -408,24 +418,16 @@ function hideFormForInitialSubmission() { const form = document.querySelector("web-client-form > form"); if (form === null) { pollAttempts++; - if (pollAttempts === maxScreenPolls) { + const elapsedTime = pollAttempts * SCREEN_POLL_INTERVAL_MS; + + if (elapsedTime >= pollingTimeoutMs) { + restoreOpacity(); window.clearInterval(intervalId); } return; } - // Now that we know the container exists, it's safe to hide it - rootNode.style.setProperty("--coder-opacity-multiplier", "0"); - - // 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("--coder-opacity-multiplier", "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. From ef4c87e48e3e515ed10948101492a0fad6abdb0f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 20:45:39 +0000 Subject: [PATCH 32/96] fix: simplify code for hiding form --- windows-rdp/devolutions-patch.js | 39 +++++++------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index fd97cfb..5adcd38 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -408,38 +408,15 @@ function hideFormForInitialSubmission() { rootNode.style.setProperty(opacityVariableName, "1"); }; - /** @type {number | undefined} */ - let intervalId = undefined; - const pollingTimeoutMs = 5_000; - let pollAttempts = 0; - - const checkIfSafeToHideForm = () => { - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - if (form === null) { - pollAttempts++; - const elapsedTime = pollAttempts * SCREEN_POLL_INTERVAL_MS; - - if (elapsedTime >= pollingTimeoutMs) { - restoreOpacity(); - window.clearInterval(intervalId); - } - - return; - } - - // 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. - window.setTimeout(restoreOpacity, 5_000); - form.addEventListener("submit", restoreOpacity, { once: true }); - }; + // 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. - intervalId = window.setInterval( - checkIfSafeToHideForm, - SCREEN_POLL_INTERVAL_MS, - ); + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener("submit", restoreOpacity, { once: true }); + window.setTimeout(restoreOpacity, 5_000); } function setupFormOverrides() { From a9a75b675faeeb00c7ee4d655fc1bbc211942a47 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:01:11 +0000 Subject: [PATCH 33/96] fix: add more changes to opacity logic --- windows-rdp/devolutions-patch.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 5adcd38..3e08f76 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -360,7 +360,7 @@ function setupAlwaysOnStyles() { function hideFormForInitialSubmission() { const styleId = "coder-patch--styles-initial-submission"; - const opacityVariableName = "--coder-opacity-multiplier"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; /** @type {HTMLStyleElement | null} */ let styleContainer = document.querySelector("#" + styleId); @@ -378,12 +378,12 @@ function hideFormForInitialSubmission() { but the rest of the function should be in charge of making the form container visible again if something goes wrong during setup. */ - $${opacityVariableName}: 0; + $${cssOpacityVariableName}: 0; } /* web-client-form is the container for the main session form */ web-client-form { - opacity: calc(100% * var($${opacityVariableName})) !important; + opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; @@ -405,18 +405,26 @@ function hideFormForInitialSubmission() { // 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(opacityVariableName, "1"); + 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. + window.setTimeout(restoreOpacity, 5_000); /** @type {HTMLFormElement | null} */ const form = document.querySelector("web-client-form > form"); - form?.addEventListener("submit", restoreOpacity, { once: true }); - window.setTimeout(restoreOpacity, 5_000); + 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 }, + ); } function setupFormOverrides() { From f3c30abeb431baef522409cd5ec26e65d76a5bfb Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:03:02 +0000 Subject: [PATCH 34/96] fix: make form hiding logic run on webpage opening --- windows-rdp/devolutions-patch.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 3e08f76..e700330 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -427,18 +427,14 @@ function hideFormForInitialSubmission() { ); } -function setupFormOverrides() { - hideFormForInitialSubmission(); - setupFormDetection(); -} - // Always safe to call this 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", setupFormOverrides); + document.addEventListener("DOMContentLoaded", setupFormDetection); } else { - setupFormOverrides(); + setupFormDetection(); } From 8aff87fdf79ffc1ae9f03dfae92f7a9ecc819a95 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:20:42 +0000 Subject: [PATCH 35/96] fix: add logic for hiding the dropdown of protocol options --- windows-rdp/devolutions-patch.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index e700330..c9e25da 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -381,8 +381,14 @@ function hideFormForInitialSubmission() { $${cssOpacityVariableName}: 0; } - /* web-client-form is the container for the main session form */ - web-client-form { + /* + 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 { opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; From b09c4cb0841e927a4fb82c262d9a81630d5685a7 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:35:53 +0000 Subject: [PATCH 36/96] fix: speed up code for filling in form --- windows-rdp/devolutions-patch.js | 99 +++++++++----------------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index c9e25da..86198a8 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -74,9 +74,9 @@ const formFieldEntries = { }; /** - * Handles typing in the values for the input form, dispatching each character - * as an event. This function assumes that all characters in the input will be - * UTF-8. + * 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 @@ -94,75 +94,32 @@ const formFieldEntries = { * @returns {Promise} */ function setInputValue(inputField, inputText) { - const continueEventName = "coder-patch--continue"; - - const keyboardInputPromise = /** @type {Promise} */ ( - new Promise((resolve, reject) => { - if (inputText === "") { - resolve(); - return; - } - - // -1 indicates a "pre-write" for clearing out the input before trying to - // write new text to it - let i = -1; - - // requestAnimationFrame is not capable of giving back values of 0 for its - // task IDs. Good default value to ensure that we don't need if statements - // when trying to cancel anything - let currentAnimationId = 0; - - // Super easy to pool the same event objects, because the events don't - // have any custom, context-specific values on them, and they're - // restricted to this one callback. - const continueEvent = new CustomEvent(continueEventName); - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - - /** @returns {void} */ - const handleNextCharIndex = () => { - if (i === inputText.length) { - resolve(); - return; - } - - const currentChar = inputText[i]; - if (i !== -1 && currentChar === undefined) { - throw new Error("Went out of bounds"); - } - - try { - inputField.addEventListener( - continueEventName, - () => { - i++; - currentAnimationId = - window.requestAnimationFrame(handleNextCharIndex); - }, - { once: true }, - ); - - if (i === -1) { - inputField.value = ""; - } else { - inputField.value = inputField.value + currentChar; - } - - inputField.dispatchEvent(inputEvent); - inputField.dispatchEvent(continueEvent); - } catch (err) { - cancelAnimationFrame(currentAnimationId); - reject(err); - } - }; - - currentAnimationId = window.requestAnimationFrame(handleNextCharIndex); - }) - ); + 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 = () => { + resolve(); + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + }; + + 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, + }); - return keyboardInputPromise; + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); } /** From 5f418c325321f88f93a6b6132e60e4739ebc78d0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:51:21 +0000 Subject: [PATCH 37/96] docs: add comments about necessary double dollar signs --- windows-rdp/devolutions-patch.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 86198a8..9b77a8e 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -103,9 +103,9 @@ function setInputValue(inputField, inputText) { }, 3_000); const handleSuccessfulDispatch = () => { - resolve(); window.clearTimeout(timeoutId); inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); }; inputField.addEventListener("input", handleSuccessfulDispatch); @@ -334,6 +334,8 @@ function hideFormForInitialSubmission() { 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; } @@ -346,6 +348,9 @@ function hideFormForInitialSubmission() { */ web-client-form, body > div.p-overlay { + /* + Double dollar sign needed to avoid Terraform script false positives + */ opacity: calc(100% * var($${cssOpacityVariableName})) !important; } `; From b283ac3129f7f5c946f9788be440c69549d024a5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 25 Jun 2024 21:54:13 +0000 Subject: [PATCH 38/96] docs: fix misleading typo in comment --- windows-rdp/devolutions-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 9b77a8e..0bd91e4 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -395,7 +395,7 @@ function hideFormForInitialSubmission() { ); } -// Always safe to call this immediately because even if the Angular app isn't +// 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(); From aebf095075902036ac4141c86d567cf00fcdaff0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 14:37:14 +0000 Subject: [PATCH 39/96] refactor: clean up patch logic for clarity --- windows-rdp/devolutions-patch.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 0bd91e4..a1e9da4 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -16,7 +16,7 @@ * 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 it will save you some headache. + * 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 @@ -240,16 +240,12 @@ function setupFormDetection() { /** @type {HTMLFormElement | null} */ const latestForm = document.querySelector("web-client-form > form"); - if (latestForm === null) { - formValueFromLastMutation = null; - return; - } - // 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 - if (formValueFromLastMutation === null) { + const mounted = formValueFromLastMutation === null && latestForm !== null; + if (mounted) { autoSubmitForm(latestForm); } @@ -364,7 +360,10 @@ function hideFormForInitialSubmission() { // and over. const rootNode = document.querySelector(":root"); if (!(rootNode instanceof HTMLHtmlElement)) { - styleContainer.innerHTML = ""; + // 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; } @@ -380,6 +379,9 @@ function hideFormForInitialSubmission() { // 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} */ From f335cd343d6b4f2a97f172aef1dee8ccda62876e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:00:40 +0000 Subject: [PATCH 40/96] fix: update type definitions for helpers --- test.ts | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/test.ts b/test.ts index 37e0805..faac253 100644 --- a/test.ts +++ b/test.ts @@ -76,27 +76,22 @@ export const execContainer = async ( }; }; +type TerraformStateResource = { + type: string; + name: string; + provider: string; + instances: [{ attributes: Record }]; +}; + 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 { @@ -168,9 +163,11 @@ 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 ( +export const runTerraformApply = async < + TVars extends Readonly>, +>( dir: string, - vars: Record, + vars: TVars, ): Promise => { const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; const env = {}; @@ -221,5 +218,5 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => { "Content-Type": "application/json", }, status: statusCode, - }) -} \ No newline at end of file + }); +}; From 33d44fdf17816ccde964271a6a313fa2ff431585 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:00:57 +0000 Subject: [PATCH 41/96] fix: remove unneeded any types --- vscode-desktop/main.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-desktop/main.test.ts b/vscode-desktop/main.test.ts index 53fba96..a3ab6bb 100644 --- a/vscode-desktop/main.test.ts +++ b/vscode-desktop/main.test.ts @@ -21,7 +21,7 @@ describe("vscode-desktop", async () => { "vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN", ); - const resources: any = state.resources; + const resources = state.resources; expect(resources[1].instances[0].attributes.order).toBeNull(); }); @@ -31,7 +31,7 @@ describe("vscode-desktop", async () => { order: "22", }); - const resources: any = state.resources; + const resources = state.resources; expect(resources[1].instances[0].attributes.order).toBe(22); }); }); From b2807640aaee8ccdb79de2891281e0c9c2bc01d6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 16:01:08 +0000 Subject: [PATCH 42/96] wip: commit progress on main test file --- windows-rdp/main.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 windows-rdp/main.test.ts diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts new file mode 100644 index 0000000..b6d0e09 --- /dev/null +++ b/windows-rdp/main.test.ts @@ -0,0 +1,61 @@ +import { beforeAll, 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", () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + }); + + it("Patches the Devolutions Angular app's .html file (after it has been bundled) to include an import for the custom JS file", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + + throw new Error("Not implemented yet"); + }); + + it("Injects the Terraform 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( + 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( + import.meta.dir, + { + agent_id: "foo", + resource_id: "bar", + admin_username: customUsername, + admin_password: customPassword, + }, + ); + }); +}); From 83ecba2293e4bdbcf662af124baa78c145f02202 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 17:21:39 +0000 Subject: [PATCH 43/96] wip: commit current progress --- windows-rdp/main.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index b6d0e09..402df53 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -22,7 +22,18 @@ describe("Web RDP", () => { }); }); - it("Patches the Devolutions Angular app's .html file (after it has been bundled) to include an import for the custom JS file", async () => { + it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", + }); + }); + + /** + * @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(import.meta.dir, { agent_id: "foo", resource_id: "bar", @@ -31,7 +42,7 @@ describe("Web RDP", () => { throw new Error("Not implemented yet"); }); - it("Injects the Terraform username and password into the JS patch file", async () => { + 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 From 264584e673f6d6c5b31f74fe24fb90119fe245fd Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 17:59:12 +0000 Subject: [PATCH 44/96] fix: make comments for test helpers exportable --- test.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test.ts b/test.ts index faac253..2601d0e 100644 --- a/test.ts +++ b/test.ts @@ -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, @@ -100,10 +102,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 = ( +/** + * 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 = ( state: TerraformState, type: T, name?: string, @@ -126,9 +129,10 @@ export const findResourceInstance = ( return resource.instances[0].attributes as any; }; -// testRequiredVariables creates a test-case -// for each variable provided and ensures that -// the apply fails without it. +/** + * Creates a test-case for each variable provided and ensures that the apply + * fails without it. + */ export const testRequiredVariables = ( dir: string, vars: Record, @@ -160,9 +164,11 @@ 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. +/** + * 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>, >( @@ -200,7 +206,9 @@ export const runTerraformApply = async < 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, From de00f6334f6c79af6f22ff59958dd012802b2837 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 26 Jun 2024 19:00:42 +0000 Subject: [PATCH 45/96] chore: add type parameter for testRequiredVariables --- test.ts | 4 ++-- windows-rdp/main.test.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test.ts b/test.ts index 2601d0e..8052404 100644 --- a/test.ts +++ b/test.ts @@ -133,9 +133,9 @@ export const findResourceInstance = ( * Creates a test-case for each variable provided and ensures that the apply * fails without it. */ -export const testRequiredVariables = ( +export const testRequiredVariables = >( dir: string, - vars: Record, + vars: TVars, ) => { // Ensures that all required variables are provided. it("required variables", async () => { diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 402df53..4e34285 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, test } from "bun:test"; +import { describe, expect, it, test } from "bun:test"; import { executeScriptInContainer, runTerraformApply, @@ -13,13 +13,11 @@ type TestVariables = Readonly<{ admin_password?: string; }>; -describe("Web RDP", () => { - beforeAll(async () => { - await runTerraformInit(import.meta.dir); - testRequiredVariables(import.meta.dir, { - agent_id: "foo", - resource_id: "bar", - }); +describe("Web RDP", async () => { + await runTerraformInit(import.meta.dir); + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + resource_id: "bar", }); it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { @@ -27,6 +25,8 @@ describe("Web RDP", () => { agent_id: "foo", resource_id: "bar", }); + + throw new Error("Not implemented yet"); }); /** From 7d366ff92aaaa1c55710cd72ee19094f22e126cd Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:20:00 +0000 Subject: [PATCH 46/96] chore: add first finished test --- windows-rdp/main.test.ts | 76 ++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 4e34285..64738e0 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, test } from "bun:test"; import { + JsonValue, + TerraformState, executeScriptInContainer, runTerraformApply, runTerraformInit, @@ -13,6 +15,11 @@ type TestVariables = Readonly<{ admin_password?: string; }>; +/** + * @todo It would be nice if we had a way to verify that the Devolutions root + * HTML file is modified to include the import for the patched Coder script, + * but the current test setup doesn't really make that viable + */ describe("Web RDP", async () => { await runTerraformInit(import.meta.dir); testRequiredVariables(import.meta.dir, { @@ -29,21 +36,38 @@ describe("Web RDP", async () => { 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(import.meta.dir, { - agent_id: "foo", - resource_id: "bar", - }); + it("Injects Terraform's username and password into the JS patch file", async () => { + const findInstancesScript = (state: TerraformState): string | null => { + let instancesScript: string | null = null; + for (const resource of state.resources) { + if (resource.type !== "coder_script") { + continue; + } - throw new Error("Not implemented yet"); - }); + for (const instance of resource.instances) { + if (instance.attributes.display_name === "windows-rdp") { + instancesScript = instance.attributes.script; + } + } + } - it("Injects Terraform's username and password into the JS patch file", async () => { - throw new Error("Not implemented yet"); + return instancesScript; + }; + + /** + * Using a regex as a quick-and-dirty way to get at the username and + * password values. + * + * Tried going through the trouble of extracting out the form entries + * variable from the main output, converting it from Prettier/JS-based JSON + * text to universal JSON text, and exposing it as a parsed JSON value. That + * got to be too much, though. + * + * Written and tested via Regex101 + * @see {@link https://regex101.com/r/UMgQpv/2} + */ + const formEntryValuesRe = + /^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?.+?)",$.*?^};$/ms; // Test that things work with the default username/password const defaultState = await runTerraformApply( @@ -54,19 +78,35 @@ describe("Web RDP", async () => { }, ); - const output = await executeScriptInContainer(defaultState, "alpine"); + const defaultInstancesScript = findInstancesScript(defaultState); + expect(defaultInstancesScript).toBeString(); + + const { username: defaultUsername, password: defaultPassword } = + formEntryValuesRe.exec(defaultInstancesScript)?.groups ?? {}; + + expect(defaultUsername).toBe("Administrator"); + expect(defaultPassword).toBe("coderRDP!"); // Test that custom usernames/passwords are also forwarded correctly - const customUsername = "crouton"; - const customPassword = "VeryVeryVeryVeryVerySecurePassword97!"; + const userDefinedUsername = "crouton"; + const userDefinedPassword = "VeryVeryVeryVeryVerySecurePassword97!"; const customizedState = await runTerraformApply( import.meta.dir, { agent_id: "foo", resource_id: "bar", - admin_username: customUsername, - admin_password: customPassword, + admin_username: userDefinedUsername, + admin_password: userDefinedPassword, }, ); + + const customInstancesScript = findInstancesScript(customizedState); + expect(customInstancesScript).toBeString(); + + const { username: customUsername, password: customPassword } = + formEntryValuesRe.exec(customInstancesScript)?.groups ?? {}; + + expect(customUsername).toBe(userDefinedUsername); + expect(customPassword).toBe(userDefinedPassword); }); }); From 6409ee2bbaf59ab5a441a4b00a5615f98bc5a039 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:23:01 +0000 Subject: [PATCH 47/96] refactor: clean up current code --- windows-rdp/main.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 64738e0..16703e3 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, test } from "bun:test"; import { - JsonValue, TerraformState, executeScriptInContainer, runTerraformApply, @@ -38,7 +37,6 @@ describe("Web RDP", async () => { it("Injects Terraform's username and password into the JS patch file", async () => { const findInstancesScript = (state: TerraformState): string | null => { - let instancesScript: string | null = null; for (const resource of state.resources) { if (resource.type !== "coder_script") { continue; @@ -46,12 +44,12 @@ describe("Web RDP", async () => { for (const instance of resource.instances) { if (instance.attributes.display_name === "windows-rdp") { - instancesScript = instance.attributes.script; + return instance.attributes.script as string; } } } - return instancesScript; + return null; }; /** @@ -61,7 +59,7 @@ describe("Web RDP", async () => { * Tried going through the trouble of extracting out the form entries * variable from the main output, converting it from Prettier/JS-based JSON * text to universal JSON text, and exposing it as a parsed JSON value. That - * got to be too much, though. + * got to be a bit too much, though. * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} From 25c90001f4d49d44bc81dc38555ee9b5f2285eff Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 17:28:13 +0000 Subject: [PATCH 48/96] docs: add comment about how regex is set up --- windows-rdp/main.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 16703e3..0a1452e 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -61,6 +61,12 @@ describe("Web RDP", async () => { * text to universal JSON text, and exposing it as a parsed JSON value. That * got to be a bit too much, though. * + * Regex is a little bit more verbose and pedantic than normal. Want to + * have some basic safety nets for validating the structure of the form + * entries variable after the JS file has had values injected. Really do + * not want the wildcard classes to overshoot and grab too much content, + * even if they're all set to lazy mode. + * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} */ From 5869eb86d47b13b1369665b70b56cd4b77354c2a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 19:42:23 +0000 Subject: [PATCH 49/96] chore: finish all initial tests --- windows-rdp/main.test.ts | 78 +++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 0a1452e..58e0544 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -1,7 +1,6 @@ -import { describe, expect, it, test } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { TerraformState, - executeScriptInContainer, runTerraformApply, runTerraformInit, testRequiredVariables, @@ -14,6 +13,25 @@ type TestVariables = Readonly<{ admin_password?: string; }>; +function findWindowsRpdScript(state: TerraformState): string | null { + for (const resource of state.resources) { + const isRdpScriptResource = + resource.type === "coder_script" && resource.name === "windows-rdp"; + + if (!isRdpScriptResource) { + continue; + } + + for (const instance of resource.instances) { + if (instance.attributes.display_name === "windows-rdp") { + return instance.attributes.script; + } + } + } + + return null; +} + /** * @todo It would be nice if we had a way to verify that the Devolutions root * HTML file is modified to include the import for the patched Coder script, @@ -26,32 +44,28 @@ describe("Web RDP", async () => { resource_id: "bar", }); - it("Installs the Devolutions Gateway Angular app locally on the machine", async () => { + it("Has the PowerShell script install Devolutions Gateway", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", resource_id: "bar", }); - throw new Error("Not implemented yet"); + const lines = findWindowsRpdScript(state) + .split("\n") + .filter(Boolean) + .map((line) => line.trimStart()); + + expect(lines).toEqual( + expect.arrayContaining([ + '$moduleName = "DevolutionsGateway"', + // Devolutions does versioning in the format year.minor.patch + expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), + "Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", + ]), + ); }); it("Injects Terraform's username and password into the JS patch file", async () => { - const findInstancesScript = (state: TerraformState): string | null => { - for (const resource of state.resources) { - if (resource.type !== "coder_script") { - continue; - } - - for (const instance of resource.instances) { - if (instance.attributes.display_name === "windows-rdp") { - return instance.attributes.script as string; - } - } - } - - return null; - }; - /** * Using a regex as a quick-and-dirty way to get at the username and * password values. @@ -82,35 +96,35 @@ describe("Web RDP", async () => { }, ); - const defaultInstancesScript = findInstancesScript(defaultState); - expect(defaultInstancesScript).toBeString(); + const defaultRdpScript = findWindowsRpdScript(defaultState); + expect(defaultRdpScript).toBeString(); const { username: defaultUsername, password: defaultPassword } = - formEntryValuesRe.exec(defaultInstancesScript)?.groups ?? {}; + formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {}; expect(defaultUsername).toBe("Administrator"); expect(defaultPassword).toBe("coderRDP!"); // Test that custom usernames/passwords are also forwarded correctly - const userDefinedUsername = "crouton"; - const userDefinedPassword = "VeryVeryVeryVeryVerySecurePassword97!"; + const customAdminUsername = "crouton"; + const customAdminPassword = "VeryVeryVeryVeryVerySecurePassword97!"; const customizedState = await runTerraformApply( import.meta.dir, { agent_id: "foo", resource_id: "bar", - admin_username: userDefinedUsername, - admin_password: userDefinedPassword, + admin_username: customAdminUsername, + admin_password: customAdminPassword, }, ); - const customInstancesScript = findInstancesScript(customizedState); - expect(customInstancesScript).toBeString(); + const customRdpScript = findWindowsRpdScript(customizedState); + expect(customRdpScript).toBeString(); const { username: customUsername, password: customPassword } = - formEntryValuesRe.exec(customInstancesScript)?.groups ?? {}; + formEntryValuesRe.exec(customRdpScript)?.groups ?? {}; - expect(customUsername).toBe(userDefinedUsername); - expect(customPassword).toBe(userDefinedPassword); + expect(customUsername).toBe(customAdminUsername); + expect(customPassword).toBe(customAdminPassword); }); }); From 90e15cd90c249616cc3d08340ce8cdb7f471c705 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 19:49:16 +0000 Subject: [PATCH 50/96] fix: update string formatting logic to make tests less likely to flake from modifications --- windows-rdp/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 58e0544..a8b4eb2 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -53,7 +53,7 @@ describe("Web RDP", async () => { const lines = findWindowsRpdScript(state) .split("\n") .filter(Boolean) - .map((line) => line.trimStart()); + .map((line) => line.trim()); expect(lines).toEqual( expect.arrayContaining([ From 05a20a9e1fa9b0ac7871089633b40070b44e2cf9 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 27 Jun 2024 20:00:44 +0000 Subject: [PATCH 51/96] docs: rewrite comment for clarity --- windows-rdp/main.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index a8b4eb2..1c739b8 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -77,9 +77,9 @@ describe("Web RDP", async () => { * * Regex is a little bit more verbose and pedantic than normal. Want to * have some basic safety nets for validating the structure of the form - * entries variable after the JS file has had values injected. Really do - * not want the wildcard classes to overshoot and grab too much content, - * even if they're all set to lazy mode. + * entries variable after the JS file has had values injected. Even with all + * the wildcard classes set to lazy mode, we want to make sure that they + * don't overshoot and grab too much content. * * Written and tested via Regex101 * @see {@link https://regex101.com/r/UMgQpv/2} From f82c7fd7a1ba08e6328a6fed9d12fb1b64579e4d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 16:51:03 +0000 Subject: [PATCH 52/96] test: set up NuGet in advance --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9e1d8fb..ffa2256 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,7 +34,7 @@ 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 = < Date: Fri, 28 Jun 2024 17:21:24 +0000 Subject: [PATCH 53/96] wip: add try/catch block --- windows-rdp/main.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index ffa2256..09622a7 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,6 +62,12 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges + try { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } catch { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force From 4ab72575ac048a93f3c8854f3a41b936830fab73 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:23:58 +0000 Subject: [PATCH 54/96] fix: remove accidental uncaught code --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 09622a7..eaac9cb 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -63,13 +63,14 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges try { + # Install-PackageProvider is required for AWS Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From 8262b290635f77d1162c0d2385813dc7ba907d2e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:34:36 +0000 Subject: [PATCH 55/96] wip: try reformatting try/catch --- windows-rdp/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index eaac9cb..8e52088 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -66,7 +66,8 @@ resource "coder_script" "windows-rdp" { # Install-PackageProvider is required for AWS Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } catch { + } + catch { # If the first command failed, assume that we're on GCP and run # Install-Module only Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force From 16f96d3693571edab9847e3d0666e75b1312acec Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 17:49:55 +0000 Subject: [PATCH 56/96] wip: add code for triggering try/catch --- windows-rdp/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 8e52088..273ad20 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -63,8 +63,9 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges try { - # Install-PackageProvider is required for AWS - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force } catch { From 78c948094ddb5de8f69fab272072b0e8ebf39321 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 18:20:46 +0000 Subject: [PATCH 57/96] wip: try reverting temporarily --- windows-rdp/main.tf | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 273ad20..919b5e4 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,17 +62,19 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges - try { - # Install-PackageProvider is required for AWS. Need to set command to - # terminate on failure so that try/catch actually triggers - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } - catch { - # If the first command failed, assume that we're on GCP and run - # Install-Module only - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - } + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + + # try { + # # Install-PackageProvider is required for AWS. Need to set command to + # # terminate on failure so that try/catch actually triggers + # Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + # } + # catch { + # # If the first command failed, assume that we're on GCP and run + # # Install-Module only + # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + # } # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From 78f91a542abe3e5fb917e2ba9f5304f904fce686 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 18:25:59 +0000 Subject: [PATCH 58/96] wip: revert back --- windows-rdp/main.tf | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 919b5e4..273ad20 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -62,19 +62,17 @@ resource "coder_script" "windows-rdp" { # Install the module with the specified version for all users # This requires administrator privileges - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - - # try { - # # Install-PackageProvider is required for AWS. Need to set command to - # # terminate on failure so that try/catch actually triggers - # Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - # } - # catch { - # # If the first command failed, assume that we're on GCP and run - # # Install-Module only - # Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force - # } + try { + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } + catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } # Construct the module path for system-wide installation $moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" From ec2c8edfb2bcc94e9fbdb1a5335f5a6a8602a4e5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 28 Jun 2024 21:06:08 +0000 Subject: [PATCH 59/96] fix: update null check and remove typo --- windows-rdp/main.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 1c739b8..24ce104 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -13,7 +13,7 @@ type TestVariables = Readonly<{ admin_password?: string; }>; -function findWindowsRpdScript(state: TerraformState): string | null { +function findWindowsRdpScript(state: TerraformState): string | null { for (const resource of state.resources) { const isRdpScriptResource = resource.type === "coder_script" && resource.name === "windows-rdp"; @@ -50,8 +50,8 @@ describe("Web RDP", async () => { resource_id: "bar", }); - const lines = findWindowsRpdScript(state) - .split("\n") + const lines = findWindowsRdpScript(state) + ?.split("\n") .filter(Boolean) .map((line) => line.trim()); @@ -96,7 +96,7 @@ describe("Web RDP", async () => { }, ); - const defaultRdpScript = findWindowsRpdScript(defaultState); + const defaultRdpScript = findWindowsRdpScript(defaultState); expect(defaultRdpScript).toBeString(); const { username: defaultUsername, password: defaultPassword } = @@ -118,7 +118,7 @@ describe("Web RDP", async () => { }, ); - const customRdpScript = findWindowsRpdScript(customizedState); + const customRdpScript = findWindowsRdpScript(customizedState); expect(customRdpScript).toBeString(); const { username: customUsername, password: customPassword } = From d9d1be08a320ae72baedd2b88daaf3afd8629d73 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 14:05:40 +0000 Subject: [PATCH 60/96] fix: update README for RDP --- windows-rdp/README.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 6320ca6..c00e422 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -1,25 +1,40 @@ --- display_name: Windows RDP -description: RDP Server and Web Client powered by Devolutions +description: RDP Server and Web Client, powered by Devolutions Gateway icon: ../.icons/desktop.svg maintainer_github: coder -verified: false +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) +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) +## Video + +<-- Insert demo video here --> ## Usage +For AWS: + +```tf +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.aws_instance.dev.id +} +``` + +For Google Cloud: + ```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 + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } ``` @@ -30,6 +45,4 @@ module "windows_rdp" { ## 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. \ No newline at end of file +- [ ] Test on Microsoft Azure. From a381c3ee29c750b01829024b06a295552e7de324 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 14:14:53 +0000 Subject: [PATCH 61/96] fix: update structure of README for linter --- windows-rdp/README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index c00e422..5d86082 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -11,13 +11,29 @@ 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). +```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 = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.aws_instance.dev.id +} +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "github.com/coder/modules//windows-rdp" + agent_id = resource.coder_agent.main.id + resource_id = resource.google_compute_instance.dev[0].id +} +``` + ## Video <-- Insert demo video here --> -## Usage +## Examples -For AWS: +### With AWS ```tf module "windows_rdp" { @@ -28,7 +44,7 @@ module "windows_rdp" { } ``` -For Google Cloud: +### With Google Cloud ```tf module "windows_rdp" { @@ -39,10 +55,6 @@ module "windows_rdp" { } ``` -## Tested on - -- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143) - ## Roadmap - [ ] Test on Microsoft Azure. From c59eb0c0cc0093c8c3e69b22bc334c07b181af3d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 1 Jul 2024 10:22:22 -0400 Subject: [PATCH 62/96] chore: add new video to README --- windows-rdp/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 5d86082..a050854 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -19,17 +19,11 @@ module "windows_rdp" { agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } -module "windows_rdp" { - count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" - agent_id = resource.coder_agent.main.id - resource_id = resource.google_compute_instance.dev[0].id -} ``` ## Video -<-- Insert demo video here --> +https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02 ## Examples From fd2f91c0434f69db656d20e95714e00b48b38c75 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 18:56:42 +0000 Subject: [PATCH 63/96] fix: remove commented-out code --- windows-rdp/main.tf | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 273ad20..9de4783 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -152,26 +152,3 @@ resource "coder_app" "rdp-docs" { 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 -# } -# } From b4153a6aaa5414479cde9cdc48662d6288be89ea Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:09:43 +0000 Subject: [PATCH 64/96] refactor: split off Windows script logic into separate file --- windows-rdp/main.tf | 97 ++------------------------ windows-rdp/windows-installation.tftpl | 88 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 windows-rdp/windows-installation.tftpl diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 9de4783..cd52c67 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,99 +34,12 @@ 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 = <', "$patch") | Set-Content $devolutionsHtml - } - } - - Set-AdminPassword -adminPassword "${var.admin_password}" - Configure-RDP - Install-DevolutionsGateway - Patch-Devolutions-HTML - - EOF + script = templatefile("./windows-installation.tftpl", { + CODER_USERNAME : var.admin_username, + CODER_PASSWORD : var.admin_password, + }) -run_on_start = true + run_on_start = true } resource "coder_app" "windows-rdp" { diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/windows-installation.tftpl new file mode 100644 index 0000000..fc0404a --- /dev/null +++ b/windows-rdp/windows-installation.tftpl @@ -0,0 +1,88 @@ +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 the module with the specified version for all users +# This requires administrator privileges +try { + # Install-PackageProvider is required for AWS. Need to set command to + # terminate on failure so that try/catch actually triggers + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force +} +catch { + # If the first command failed, assume that we're on GCP and run + # Install-Module only + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force +} + +# 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 = '' + +# 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('', "$patch") | Set-Content $devolutionsHtml +} +} + +Set-AdminPassword -adminPassword "${var.admin_password}" +Configure-RDP +Install-DevolutionsGateway +Patch-Devolutions-HTML From 49f060549ee48b0e305b7b2039fa52d8c6cc730e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:14:05 +0000 Subject: [PATCH 65/96] fix: update TF import --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index cd52c67..cccaf85 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,7 +34,7 @@ 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 = templatefile("./windows-installation.tftpl", { + script = templatefile("${path.module}/./windows-installation.tftpl", { CODER_USERNAME : var.admin_username, CODER_PASSWORD : var.admin_password, }) From a8580fe6b92389f2e985136301b83a167ebfe826 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 19:24:47 +0000 Subject: [PATCH 66/96] fix: update object definition for top-level templatefile --- windows-rdp/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index cccaf85..06f2c17 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,9 +34,9 @@ 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 = templatefile("${path.module}/./windows-installation.tftpl", { - CODER_USERNAME : var.admin_username, - CODER_PASSWORD : var.admin_password, + script = templatefile("${path.module}/windows-installation.tftpl", { + CODER_USERNAME = var.admin_username, + CODER_PASSWORD = var.admin_password, }) run_on_start = true From b23d85327ceb56707bc2a62cf3ce8dc488484c31 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 20:11:40 +0000 Subject: [PATCH 67/96] refactor: try extracting main script into separate template file --- windows-rdp/main.tf | 9 +++++++-- windows-rdp/windows-installation.tftpl | 11 ++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 06f2c17..f47e94e 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -34,9 +34,14 @@ 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 = templatefile("${path.module}/windows-installation.tftpl", { - CODER_USERNAME = var.admin_username, - CODER_PASSWORD = var.admin_password, + admin_username = var.admin_username + admin_password = var.admin_password + patch_file_contents = templatefile("${path.module}/devolutions-patch.js", { + CODER_USERNAME = var.admin_username + CODER_PASSWORD = var.admin_password + }) }) run_on_start = true diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/windows-installation.tftpl index fc0404a..1b7ab48 100644 --- a/windows-rdp/windows-installation.tftpl +++ b/windows-rdp/windows-installation.tftpl @@ -3,9 +3,9 @@ function Set-AdminPassword { [string]$adminPassword ) # Set admin password - Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) + Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user - Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser + Get-LocalUser -Name "${admin_username}" | Enable-LocalUser } function Configure-RDP { @@ -69,10 +69,7 @@ $patch = '' # 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, -})} +${patch_file_contents} '@ | Set-Content "$root\coder.js" # Only inject the src if we have not before. @@ -82,7 +79,7 @@ if ($isPatched -eq $null) { } } -Set-AdminPassword -adminPassword "${var.admin_password}" +Set-AdminPassword -adminPassword "${admin_password}" Configure-RDP Install-DevolutionsGateway Patch-Devolutions-HTML From 3f8f6181e0a67115145cfd8cf00c8bc58f291600 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 1 Jul 2024 20:31:43 +0000 Subject: [PATCH 68/96] refactor: clean up final code --- windows-rdp/devolutions-patch.js | 9 ++++----- windows-rdp/main.tf | 6 +++++- ...lation.tftpl => powershell-installation-script.tftpl} | 0 3 files changed, 9 insertions(+), 6 deletions(-) rename windows-rdp/{windows-installation.tftpl => powershell-installation-script.tftpl} (100%) diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index a1e9da4..020a40f 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -12,11 +12,10 @@ * - 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. + * placeholder syntax via the dollar sign. The Terraform file is treating this + * as a template file, and because it also uses a similar syntax, there's a + * risk that some values will trigger false positives. If a template literal + * must be used, be sure to use a double dollar sign to escape things. * - 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 diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index f47e94e..563e10f 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -35,9 +35,13 @@ resource "coder_script" "windows-rdp" { display_name = "windows-rdp" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons - script = templatefile("${path.module}/windows-installation.tftpl", { + script = templatefile("${path.module}/powershell-installation-script.tftpl", { admin_username = var.admin_username admin_password = var.admin_password + + # Wanted to have this be in the powershell template file, but Terraform + # doesn't allow recursive calls to the templatefile function. Have to feed + # results of the JS template replace into the powershell template patch_file_contents = templatefile("${path.module}/devolutions-patch.js", { CODER_USERNAME = var.admin_username CODER_PASSWORD = var.admin_password diff --git a/windows-rdp/windows-installation.tftpl b/windows-rdp/powershell-installation-script.tftpl similarity index 100% rename from windows-rdp/windows-installation.tftpl rename to windows-rdp/powershell-installation-script.tftpl From 894e507bb36d9a169e4eba790d31bed168167560 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:19:16 +0000 Subject: [PATCH 69/96] fix: add verison number to rdp script --- windows-rdp/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index a050854..2ddf3f8 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -14,8 +14,9 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de ```tf # AWS example. See below for examples of using this module with other providers module "windows_rdp" { + source = "github.com/coder/modules/windows-rdp" + version = "1.0.15" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } From d98bfcb20b75b5eb85b9dab87a49379fc446695c Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:21:45 +0000 Subject: [PATCH 70/96] fix: add versioning to all code snippets --- windows-rdp/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 2ddf3f8..d80fc42 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -14,8 +14,8 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de ```tf # AWS example. See below for examples of using this module with other providers module "windows_rdp" { - source = "github.com/coder/modules/windows-rdp" - version = "1.0.15" + 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 @@ -32,8 +32,9 @@ https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a ```tf module "windows_rdp" { + source = "registry.coder.com/coder/module/windows-rdp" + version = "1.0.16" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } @@ -43,8 +44,9 @@ module "windows_rdp" { ```tf module "windows_rdp" { + source = "registry.coder.com/coder/module/windows-rdp" + version = "1.0.16" count = data.coder_workspace.me.start_count - source = "github.com/coder/modules//windows-rdp" agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } From aebdc9b434cbaf768edf29a21c67e8b442495825 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 15:26:40 +0000 Subject: [PATCH 71/96] fix: update docs link --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 563e10f..1308ac0 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -71,6 +71,6 @@ resource "coder_app" "rdp-docs" { 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" + url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop" external = true } From e8ee02c0445e7ab18a1c963f49946469b51697be Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 2 Jul 2024 16:02:50 +0000 Subject: [PATCH 72/96] fix: update URL for RDP icon --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 1308ac0..8d874fa 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -33,7 +33,7 @@ variable "admin_password" { 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 + icon = "/icon/desktop.svg" script = templatefile("${path.module}/powershell-installation-script.tftpl", { admin_username = var.admin_username From bf175a1247b9006a9e659584b09a934a799a728f Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 2 Jul 2024 12:10:22 -0400 Subject: [PATCH 73/96] fix: update SVG icon URL for RDP module Accidentally missed one of the URL before merging the main module. --- windows-rdp/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 8d874fa..fb09c48 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -56,7 +56,7 @@ resource "coder_app" "windows-rdp" { slug = "web-rdp" display_name = "Web RDP" url = "http://localhost:7171" - icon = "https://svgur.com/i/158F.svg" + icon = "/icon/desktop.svg" subdomain = true healthcheck { From 883741244b6c9085ac30cb86404c22bbbc8a8cd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:04:31 +0300 Subject: [PATCH 74/96] chore: bump version to 1.0.16 in README.md files (#265) Co-authored-by: matifali --- code-server/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code-server/README.md b/code-server/README.md index e1ca7a2..8132307 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "code-server" { ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id install_version = "4.8.3" } @@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.15" + version = "1.0.16" agent_id = coder_agent.example.id offline = true } From b1f81afa7f35b6f8738d4d4b738cd17c9d414ecc Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 3 Jul 2024 22:34:20 +0300 Subject: [PATCH 75/96] docs(windows-rdp): make sure video renders correctly --- windows-rdp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index d80fc42..6b38eab 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -24,7 +24,7 @@ module "windows_rdp" { ## Video -https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02 +[![Video](https://img.youtube.com/vi/VIDEO_ID/0.jpg)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02) ## Examples From d77ad8ac63d627f6c9160bf6fc0203b3e12795a9 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 3 Jul 2024 22:38:22 +0300 Subject: [PATCH 76/96] fixup! --- windows-rdp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 6b38eab..76aae1c 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -24,7 +24,7 @@ module "windows_rdp" { ## Video -[![Video](https://img.youtube.com/vi/VIDEO_ID/0.jpg)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02) +[![Video](../.icons/desktop.svg)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02) ## Examples From da4a561cb52ababcb6dd34ea525233eab252831c Mon Sep 17 00:00:00 2001 From: Maxime Brunet Date: Wed, 3 Jul 2024 21:25:19 +0000 Subject: [PATCH 77/96] fix(code-server): add variable for subdomain option (#267) --- code-server/main.tf | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/code-server/main.tf b/code-server/main.tf index 30b705c..9961693 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -113,6 +113,15 @@ variable "auto_install_extensions" { default = false } +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = false +} + resource "coder_script" "code-server" { agent_id = var.agent_id display_name = "code-server" @@ -154,7 +163,7 @@ resource "coder_app" "code-server" { display_name = var.display_name url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}" icon = "/icon/code.svg" - subdomain = false + subdomain = var.subdomain share = var.share order = var.order From a2c29ace0a9194967b92cfdec2c56c8dcd5f8199 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 10 Jul 2024 14:01:42 +0000 Subject: [PATCH 78/96] fix: update HTML for video tag --- windows-rdp/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 76aae1c..c551f69 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -24,7 +24,16 @@ module "windows_rdp" { ## Video -[![Video](../.icons/desktop.svg)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02) + ## Examples From a40f2b86c334465328335a39604010cdf7847360 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 10 Jul 2024 22:53:09 +0000 Subject: [PATCH 79/96] chore: add custom thumbnail for video --- windows-rdp/README.md | 11 +---------- .../video-thumbnails/video-thumbnail.png | Bin 0 -> 100943 bytes 2 files changed, 1 insertion(+), 10 deletions(-) create mode 100644 windows-rdp/video-thumbnails/video-thumbnail.png diff --git a/windows-rdp/README.md b/windows-rdp/README.md index c551f69..a124eb0 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -24,16 +24,7 @@ module "windows_rdp" { ## Video - +[![Video](./video-thumbnails/video-thumbnail.png)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02) ## Examples diff --git a/windows-rdp/video-thumbnails/video-thumbnail.png b/windows-rdp/video-thumbnails/video-thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..f37d65da937b043baaca3879514ba8757c2bf6e5 GIT binary patch literal 100943 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 literal 0 HcmV?d00001 From 889186d55361bae14242c0af0532cb0306773425 Mon Sep 17 00:00:00 2001 From: Stephen Kirby Date: Tue, 16 Jul 2024 19:29:19 +0000 Subject: [PATCH 80/96] fixed url schema for webrdp example --- windows-rdp/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index a124eb0..1201130 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -32,7 +32,7 @@ module "windows_rdp" { ```tf module "windows_rdp" { - source = "registry.coder.com/coder/module/windows-rdp" + source = "registry.coder.com/modules/windows-rdp/coder" version = "1.0.16" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id @@ -44,7 +44,7 @@ module "windows_rdp" { ```tf module "windows_rdp" { - source = "registry.coder.com/coder/module/windows-rdp" + source = "registry.coder.com/modules/windows-rdp/coder" version = "1.0.16" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id From f7fa1458551cc102667ef655397f136fa1271330 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 17:56:25 +0000 Subject: [PATCH 81/96] docs: update some wording for clarity --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b67594..48a96a3 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ Modules -[Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise) +[Module Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise) [![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder) [![license](https://img.shields.io/github/license/coder/modules)](./LICENSE) -Modules extend Templates to create reusable components for your development environment. +Modules extend Coder Templates to create reusable components for your development environment. e.g. From cd010baac8475c428d594bd678e4aab72aa01809 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 17:59:04 +0000 Subject: [PATCH 82/96] chore: switch codebase to use TS strict mode --- slackme/main.test.ts | 13 +++++++++---- test.ts | 8 +++++++- tsconfig.json | 1 + vscode-desktop/main.test.ts | 10 ++++++---- windows-rdp/main.test.ts | 16 ++++++++-------- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/slackme/main.test.ts b/slackme/main.test.ts index 402a690..eca4f5d 100644 --- a/slackme/main.test.ts +++ b/slackme/main.test.ts @@ -126,7 +126,10 @@ const assertSlackMessage = async (opts: { durationMS?: number; output: string; }) => { - let url: URL; + // Have to use non-null assertion because TS can't tell when the fetch + // function will run + let url!: URL; + const fakeSlackHost = serve({ fetch: (req) => { url = new URL(req.url); @@ -138,15 +141,16 @@ const assertSlackMessage = async (opts: { }, port: 0, }); + const { instance, id } = await setupContainer( "alpine/curl", - opts.format && { - slack_message: opts.format, - }, + opts.format ? { slack_message: opts.format } : undefined, ); + await writeCoder(id, "echo 'token'"); let exec = await execContainer(id, ["sh", "-c", instance.script]); expect(exec.exitCode).toBe(0); + exec = await execContainer(id, [ "sh", "-c", @@ -154,6 +158,7 @@ const assertSlackMessage = async (opts: { fakeSlackHost.hostname }:${fakeSlackHost.port}" slackme ${opts.command}`, ]); + expect(exec.stderr.trim()).toBe(""); expect(url.pathname).toEqual("/api/chat.postMessage"); expect(url.searchParams.get("channel")).toEqual("token"); diff --git a/test.ts b/test.ts index b338205..41eb9c1 100644 --- a/test.ts +++ b/test.ts @@ -149,19 +149,25 @@ export const testRequiredVariables = >( it("required variables", async () => { await runTerraformApply(dir, vars); }); + const varNames = Object.keys(vars); varNames.forEach((varName) => { // Ensures that every variable provided is required! it("missing variable " + varName, async () => { - const localVars = {}; + const localVars: Record = {}; varNames.forEach((otherVarName) => { if (otherVarName !== varName) { localVars[otherVarName] = vars[otherVarName]; } }); + try { await runTerraformApply(dir, localVars); } catch (ex) { + if (!(ex instanceof Error)) { + throw new Error("Unknown error generated"); + } + expect(ex.message).toContain( `input variable \"${varName}\" is not set`, ); diff --git a/tsconfig.json b/tsconfig.json index e7b89cd..dd38e58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "esnext", "module": "esnext", + "strict": true, "allowSyntheticDefaultImports": true, "moduleResolution": "nodenext", "types": ["bun-types"] diff --git a/vscode-desktop/main.test.ts b/vscode-desktop/main.test.ts index 74c4ffb..207b492 100644 --- a/vscode-desktop/main.test.ts +++ b/vscode-desktop/main.test.ts @@ -24,9 +24,10 @@ describe("vscode-desktop", async () => { const coder_app = state.resources.find( (res) => res.type == "coder_app" && res.name == "vscode", ); + expect(coder_app).not.toBeNull(); - expect(coder_app.instances.length).toBe(1); - expect(coder_app.instances[0].attributes.order).toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBeNull(); }); it("adds folder", async () => { @@ -80,8 +81,9 @@ describe("vscode-desktop", async () => { const coder_app = state.resources.find( (res) => res.type == "coder_app" && res.name == "vscode", ); + expect(coder_app).not.toBeNull(); - expect(coder_app.instances.length).toBe(1); - expect(coder_app.instances[0].attributes.order).toBe(22); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBe(22); }); }); diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 24ce104..80cbfbe 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -99,11 +99,11 @@ describe("Web RDP", async () => { const defaultRdpScript = findWindowsRdpScript(defaultState); expect(defaultRdpScript).toBeString(); - const { username: defaultUsername, password: defaultPassword } = - formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {}; + const defaultResultsGroup = + formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {}; - expect(defaultUsername).toBe("Administrator"); - expect(defaultPassword).toBe("coderRDP!"); + expect(defaultResultsGroup.username).toBe("Administrator"); + expect(defaultResultsGroup.password).toBe("coderRDP!"); // Test that custom usernames/passwords are also forwarded correctly const customAdminUsername = "crouton"; @@ -121,10 +121,10 @@ describe("Web RDP", async () => { const customRdpScript = findWindowsRdpScript(customizedState); expect(customRdpScript).toBeString(); - const { username: customUsername, password: customPassword } = - formEntryValuesRe.exec(customRdpScript)?.groups ?? {}; + const customResultsGroup = + formEntryValuesRe.exec(customRdpScript ?? "")?.groups ?? {}; - expect(customUsername).toBe(customAdminUsername); - expect(customPassword).toBe(customAdminPassword); + expect(customResultsGroup.username).toBe(customAdminUsername); + expect(customResultsGroup.password).toBe(customAdminPassword); }); }); From 66472b01052c490bcf210166891b781799d8e759 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 18:03:12 +0000 Subject: [PATCH 83/96] chore: clean up lint file --- lint.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lint.ts b/lint.ts index db1ee9a..6652fcb 100644 --- a/lint.ts +++ b/lint.ts @@ -5,14 +5,15 @@ import grayMatter from "gray-matter"; const files = await readdir(".", { withFileTypes: true }); const dirs = files.filter( - (f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules" + (f) => + f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules", ); let badExit = false; // error reports an error to the console and sets badExit to true // so that the process will exit with a non-zero exit code. -const error = (...data: any[]) => { +const error = (...data: unknown[]) => { console.error(...data); badExit = true; }; @@ -22,7 +23,7 @@ const verifyCodeBlocks = ( res = { codeIsTF: false, codeIsHCL: false, - } + }, ) => { for (const token of tokens) { // Check in-depth. @@ -30,7 +31,12 @@ const verifyCodeBlocks = ( verifyCodeBlocks(token.items, res); continue; } + if (token.type === "list_item") { + if (token.tokens === undefined) { + throw new Error("Tokens are missing for type list_item"); + } + verifyCodeBlocks(token.tokens, res); continue; } @@ -80,8 +86,9 @@ for (const dir of dirs) { if (!data.maintainer_github) { error(dir.name, "missing maintainer_github"); } + try { - await stat(path.join(".", dir.name, data.icon)); + await stat(path.join(".", dir.name, data.icon ?? "")); } catch (ex) { error(dir.name, "icon does not exist", data.icon); } From 89bb023fa55150e4fbe426111246db071d35432f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 18:14:41 +0000 Subject: [PATCH 84/96] docs: clean up current contributing guide --- CONTRIBUTING.md | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 557171e..4794661 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,47 @@ # Contributing -To create a new module, clone this repository and run: +## Getting started + +This repo uses the [Bun runtime](https://bun.sh/) to to run all code and tests. To install Bun, you can run this command on Linux/MacOS: + +```shell +curl -fsSL https://bun.sh/install | bash +``` + +Or this command on Windows: + +```shell +powershell -c "irm bun.sh/install.ps1 | iex" +``` + +Follow the instructions to ensure that Bun is available globally. Once Bun has been installed, clone this repository. From there, run this script to create a new module: ```shell -./new.sh MODULE_NAME +./new.sh NAME_OF_NEW_MODULE ``` ## Testing a Module +> **Note:** It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR. + A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. -The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop. +The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop. -Reference existing `*.test.ts` files for implementation. +Reference the existing `*.test.ts` files to get an idea for how to set up tests. + +You can run all tests in a specific file with this command: ```shell -# Run tests for a specific module! $ bun test -t '' ``` +Or run all tests by running this command: + +```shell +$ bun test +``` + You can test a module locally by updating the source as follows ```tf @@ -26,5 +49,3 @@ module "example" { source = "git::https://github.com//.git//?ref=" } ``` - -> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR. From cbe48aa072214dc6d0eb3c7c4789a4094c6d538d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 18:54:26 +0000 Subject: [PATCH 85/96] chore: bump version to 1.0.17 in README.md files --- code-server/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code-server/README.md b/code-server/README.md index 8132307..3692d71 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "code-server" { ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id install_version = "4.8.3" } @@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub: ```tf module "code-server" { source = "registry.coder.com/modules/code-server/coder" - version = "1.0.16" + version = "1.0.17" agent_id = coder_agent.example.id offline = true } From fa4b84e8d1e04a22ad5c13df0a7dc05ef0debac8 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 19:23:39 +0000 Subject: [PATCH 86/96] docs: add publishing instructions to the contributing README --- CONTRIBUTING.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4794661..dab6e4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,3 +49,26 @@ module "example" { source = "git::https://github.com//.git//?ref=" } ``` + +## Releases + +> [!WARNING] +> Version numbers are very important for publishing to Terraform. If the version number is incorrect in any way, there is a risk that some modules could stop working. + +Much of our release process is automated. To cut a new release: + +1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases) +2. Click "Draft a new release" +3. Click the "Choose a tag" button and type a new release number in the format `v..` (e.g., `v1.18.0`). Then click "Create new tag". +4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to clean up any notes that would not be relevant to end-users (e.g., bumping dependencies). +5. Once everything looks good, click the "Publish release" button. + +Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch. + +Following that, our automated processes will handle publishing new data for [`registry.coder.com`](https://github.com/coder/registry.coder.com/): + +1. Publishing new versions to Coder's [Terraform Registry](https://registry.terraform.io/providers/coder/coder/latest) +2. Publishing new data to the [Coder Registry](https://registry.coder.com) + +> [!NOTE] +> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. As such, some changes will be made almost immediately, while others may require some time. From 6a87fd18e55cba50fda2ba14e8d76a938eae3ca8 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 20:07:59 +0000 Subject: [PATCH 87/96] fix: make attributes type more specific --- test.ts | 2 +- windows-rdp/main.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test.ts b/test.ts index 41eb9c1..8483089 100644 --- a/test.ts +++ b/test.ts @@ -90,7 +90,7 @@ type TerraformStateResource = { type: string; name: string; provider: string; - instances: [{ attributes: Record }]; + instances: [{ attributes: Record }]; }; export interface TerraformState { diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts index 80cbfbe..34a8e52 100644 --- a/windows-rdp/main.test.ts +++ b/windows-rdp/main.test.ts @@ -23,7 +23,10 @@ function findWindowsRdpScript(state: TerraformState): string | null { } for (const instance of resource.instances) { - if (instance.attributes.display_name === "windows-rdp") { + if ( + instance.attributes.display_name === "windows-rdp" && + typeof instance.attributes.script === "string" + ) { return instance.attributes.script; } } From 096cd214cec4f07f859f870d413af89f43ed6d49 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 19 Jul 2024 20:18:11 +0000 Subject: [PATCH 88/96] fix: make type def for TerraformState more specific --- test.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/test.ts b/test.ts index 8483089..6bdf9d9 100644 --- a/test.ts +++ b/test.ts @@ -90,17 +90,21 @@ type TerraformStateResource = { type: string; name: string; provider: string; - instances: [{ attributes: Record }]; + + instances: [ + { + attributes: Record; + }, + ]; }; -export interface TerraformState { - outputs: { - [key: string]: { - type: string; - value: any; - }; - }; +type TerraformOutput = { + type: string; + value: JsonValue; +}; +export interface TerraformState { + outputs: Record; resources: [TerraformStateResource, ...TerraformStateResource[]]; } From 523ad9fe236444e40cf94b2afd05e6f0fea9b952 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:26:45 -0500 Subject: [PATCH 89/96] update windows-rdp to take share variable (#274) * update windows-rdp to take share variable * of course I forgot to run fmt * make 'owner' default for share * update share var validation --- bun.lockb | Bin 9456 -> 9792 bytes windows-rdp/main.test.ts | 1 + windows-rdp/main.tf | 10 ++++++++++ 3 files changed, 11 insertions(+) diff --git a/bun.lockb b/bun.lockb index d3e22141efbd6a38f0a17c4768e00326156a716d..d9abc986f834b6eb91780ee472e14151487174cf 100755 GIT binary patch delta 1664 zcmb_cZ){Ul6uni$ZDBsg_(IEQ923VxZ!$O7Sm2~4EnoVV7{4;oE8$$6*e z{?0vrZqGS4n$9;LQY(aXPjCB$?56&Qp7nkGMPuKtfvfGSrt1%%P}(NuE0@lWl{1~> z;np%CDi5a{9_&dadV7K95 z8U!of29pMjzXGCB6Xy`!fGCFa%M2jXw&9KoD<9#q>(E&^SfCZ}} z=EzoL3`))UBZ;g#k-mtZ{ zVWFI5omrGrW1fr$JJ(iAj-L-q)t`Im%|EU@a_Gnpk&9B>wbrIjZ@;$e)!Nx))v?g` z?rZknt1bJd7iLD|_y3yRTUj$bx3DvKrO(SDX7|AzTZ8?c2qHAun=$ZpgxK z2_#)~%i;FH#X`sjvb*`-YW%Rv-8>>_IBUicPs@nMRZRa)e(=|!BH@4l_om4NE_tnoh#`~1ZL-{J{dijq3;o1Dq20L0|`;5&cGW@=)B$&_XU zBhWfwPGp)X+!lHRQ{NXOQ;*SXVgKv1K~4WuuG2r$-HLQX<{~8I<&ETs9MdIQLll5? z3gSj4hcs-I00kh&lo0W)oZ}Bm;gY-pZpqKf`E{7zRaPF4s8wo|L3?1HPrze}tmIeY zPHA%c_}X=61Rf8oAvMHUHw-8deiJgvCMCZc;mLRThLe0;;4hO^6=S2Il*mecSDyIl z@SbBYTuLHdr$*Iq2=Of?axA|)#qQOgU)}oF$B0MOsv5PL{XN2hSQXM>5KfgegcG}a zcBFPE*wmWXN4M8Mef}VBB7Bd*FAtpl^FqtkQ*WbA>wB(uZ_?G#^y5j8OSQmQZfWnU mi%#*hBRiIJI$EAdl|}^cE>oaCXodNp!w^v;nRm*znC=36eLIH$ delta 1478 zcmbtUUq}>T5dXe)*K^g|yYsZWJ@2$c3N0@$t4p$5Df=T?8W-4Am`5sET^-rOm6Q?| zQWVNS^pBvI$b!hMsGu~0Fwm&CM0$uIq6bYPD5|TOy)`=O&4KxT%>2HY*;(eBp_)(C zJ>H^%s`)J`rxq0EeX9=!W75jT?fu2 zdzzaVvm=qgSm#B7F)QL}L%OLv{VmL&lCTGRr}B#?MaI&RL}fB!yOh8vA|;tZ z%tVYrIbS=_&Fcj20#(#_Erdi(zJa;dA4F250-Xk`h$MvMcadSp%o^xmY=#}otO0l{Z{`e^rq|6F zS}hdSU~#}`GGznRN|a)Cz!7UwlpUVy)MKPdY)MoO9I{c=KAQvHBvTG3%BlgIunO*H z)mf(>q!|aoj57^S>@lblM0s~x$)Rv$FH;8k?LjHMlZFK4niGaR*$`BV;H4@mEdDDY z#La>$vR$Fcd2QTYtip9Qu#pCC7Aj3P{YNZBuU&r$X^8l!^`WE5rOiMSPOpL{n5LTs zwb7W)VMh8}_4<6?vUaHTsP2FHVysUxCBbEnQ%FGi2Y14|%Y*;9vW&6jwrKc3TQeJ3 z8F=`8?Y5yHJ|xQ1z=_0D$(kF{Exs>dVX%hOyje6O?;yOu2(;V~tC U&XY*HrEuV6UYRiPW%hdMH%; diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index fb09c48..10ece09 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -9,6 +9,15 @@ terraform { } } +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + variable "agent_id" { type = string description = "The ID of a Coder agent." @@ -53,6 +62,7 @@ resource "coder_script" "windows-rdp" { resource "coder_app" "windows-rdp" { agent_id = var.agent_id + share = var.share slug = "web-rdp" display_name = "Web RDP" url = "http://localhost:7171" From 982c75e86f51d87bf6e98ed524719c022f10e7de Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 26 Jul 2024 15:07:00 +0000 Subject: [PATCH 90/96] fix: update incorrect info in docs --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dab6e4e..c0a9c0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,14 +53,14 @@ module "example" { ## Releases > [!WARNING] -> Version numbers are very important for publishing to Terraform. If the version number is incorrect in any way, there is a risk that some modules could stop working. +> When creating a new release, make sure that your new version number is fully accurate. If a version number is incorrect or does not exist, we may end up serving incorrect/old data for our various tools and providers. Much of our release process is automated. To cut a new release: 1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases) 2. Click "Draft a new release" 3. Click the "Choose a tag" button and type a new release number in the format `v..` (e.g., `v1.18.0`). Then click "Create new tag". -4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to clean up any notes that would not be relevant to end-users (e.g., bumping dependencies). +4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to remove any notes that would not be relevant to end-users (e.g., bumping dependencies). 5. Once everything looks good, click the "Publish release" button. Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch. @@ -71,4 +71,4 @@ Following that, our automated processes will handle publishing new data for [`re 2. Publishing new data to the [Coder Registry](https://registry.coder.com) > [!NOTE] -> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. As such, some changes will be made almost immediately, while others may require some time. +> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. This data should be updated almost immediately after a new release, but other changes will take some time to propagate. From f446fbd667f8022a3cc9947210e6d22e5e6adfd7 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 29 Jul 2024 06:11:06 -0700 Subject: [PATCH 91/96] fix: typo in web RDP example (#277) --- windows-rdp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows-rdp/README.md b/windows-rdp/README.md index 1201130..e8c5a1c 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -14,7 +14,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de ```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" + source = "registry.coder.com/modules/windows-rdp/coder" version = "1.0.16" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id From 4c45d69994937700f3033f120a489e1c17c1afd6 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 14 Aug 2024 00:17:39 -0700 Subject: [PATCH 92/96] fix(code-server): handle when the extension folder does not exist yet (#278) --- code-server/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/code-server/run.sh b/code-server/run.sh index 8e068b8..9af391e 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -10,6 +10,7 @@ CODE_SERVER="${INSTALL_PREFIX}/bin/code-server" EXTENSION_ARG="" if [ -n "${EXTENSIONS_DIR}" ]; then EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}" + mkdir -p "${EXTENSIONS_DIR}" fi function run_code_server() { From 236022f870f2ff3a847dd0e51d38adb0ec869ba4 Mon Sep 17 00:00:00 2001 From: megumin Date: Sun, 1 Sep 2024 12:16:05 +0100 Subject: [PATCH 93/96] feat(git-clone): custom destination folder name (#287) --- git-clone/README.md | 17 +++++++++++++++++ git-clone/main.test.ts | 16 ++++++++++++++++ git-clone/main.tf | 8 +++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/git-clone/README.md b/git-clone/README.md index 255b3f1..5efc50e 100644 --- a/git-clone/README.md +++ b/git-clone/README.md @@ -153,3 +153,20 @@ module "git-clone" { branch_name = "feat/example" } ``` + +## Git clone with different destination folder + +By default, the repository will be cloned into a folder matching the repository name. You can use the `folder_name` attribute to change the name of the destination folder to something else. + +For example, this will clone into the `~/projects/coder/coder-dev` folder: + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" + folder_name = "coder-dev" + base_dir = "~/projects/coder" +} +``` diff --git a/git-clone/main.test.ts b/git-clone/main.test.ts index 87b0e4a..9fbd202 100644 --- a/git-clone/main.test.ts +++ b/git-clone/main.test.ts @@ -79,6 +79,22 @@ describe("git-clone", async () => { expect(state.outputs.branch_name.value).toEqual(""); }); + it("repo_dir should match base_dir/folder_name", async () => { + const url = "git@github.com:coder/coder.git"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + folder_name: "foo", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/foo"); + expect(state.outputs.folder_name.value).toEqual("foo"); + expect(state.outputs.clone_url.value).toEqual(url); + const https_url = "https://github.com/coder/coder.git"; + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + it("branch_name should not include query string", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", diff --git a/git-clone/main.tf b/git-clone/main.tf index 4af5000..0295444 100644 --- a/git-clone/main.tf +++ b/git-clone/main.tf @@ -50,6 +50,12 @@ variable "branch_name" { default = "" } +variable "folder_name" { + description = "The destination folder to clone the repository into." + type = string + default = "" +} + locals { # Remove query parameters and fragments from the URL url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "") @@ -64,7 +70,7 @@ locals { # Extract the branch name from the URL branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name # Extract the folder name from the URL - folder_name = replace(basename(local.clone_url), ".git", "") + folder_name = var.folder_name == "" ? replace(basename(local.clone_url), ".git", "") : var.folder_name # Construct the path to clone the repository clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name]) # Construct the web URL From 831f64da5614cb57fb84c5580ee9d565fe60b51d Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 4 Sep 2024 00:10:38 +0500 Subject: [PATCH 94/96] chore: remove package-lock.json and update deps (#281) --- bun.lockb | Bin 9792 -> 9792 bytes package-lock.json | 264 ---------------------------------------------- package.json | 8 +- 3 files changed, 4 insertions(+), 268 deletions(-) delete mode 100644 package-lock.json diff --git a/bun.lockb b/bun.lockb index d9abc986f834b6eb91780ee472e14151487174cf..7576953c8bd3806e197b8ca78e18dc166cd6122e 100755 GIT binary patch delta 469 zcmV;`0V@8$Ou$T#E+D)DBDoUzDyi{11YiyNK&f>m?eEwqS}GLw4{LQKgWP2!mofs;rpQ7wG4;RxZlqrTve^4Q4Tt%Ph@%vw zJgY0LaWMRn$Uw-my$~P)K(Wcg=hSInKXt}< zD?tLrkWac&vD7~|ie+eFj1-#Obfm&I&~tx2t8{1~Q82{;k;8L7kCu zQq2Un2_XA@o?mhsl-bt@hxwBb0zZ>b0tB*HTH7+!h5E~Z+F)lJQlPeoD0WyDBEMs{(3>R&9+JD8&zS;gxw7S^#&+5?lQ z1TeE$1dIX&000009g`6PCzB8d)CCIw000z|5dtTZ5C{~LN zKvqmGtJ=${dh%viGux6><|di6qsj= zIkr4%kGEJ|!|>gHG|Tq+FQHLu9R!x9`+=xDO!wxqy$~P)K=1m|+f$PH`n)3{(}L3E z46k_h?Cfi){02r?Id_$d@=CyTSjlEkb2A^V<{IkzXI+Gm| z7(k_UoUN=(((FJ>>rK!BzXV;A%BhDtO?YQc^islpq+8gz6squ{{0?QQoweT5kq-=Y z*;jE~0kUL@GI^lUCtH&c0zZ>b0tB*HTGcGfe5E~Z+FfK7TlPeoD0Wgza8x{jH zE;X}w8`A*;F)lN+P95X}0Wgz*ARn_*A^rjcFfKAVvs5Gm0R%BFGBA@|B}E4_E;BB4 JXL^&N6qsGB$qoPj diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1010942..0000000 --- a/package-lock.json +++ /dev/null @@ -1,264 +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": "^3.2.5", - "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 - } - } -} diff --git a/package.json b/package.json index f3136b1..eea421d 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,15 @@ "update-version": "./update-version.sh" }, "devDependencies": { - "bun-types": "^1.0.18", + "bun-types": "^1.1.23", "gray-matter": "^4.0.3", - "marked": "^12.0.0", - "prettier": "^3.2.5", + "marked": "^12.0.2", + "prettier": "^3.3.3", "prettier-plugin-sh": "^0.13.1", "prettier-plugin-terraform-formatter": "^1.2.1" }, "peerDependencies": { - "typescript": "^5.3.3" + "typescript": "^5.5.4" }, "prettier": { "plugins": [ From 834ffde03260156a2c2a5d5e899d6edb66cfd3bb Mon Sep 17 00:00:00 2001 From: Sebastian <85109829+Seppdo@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:28:16 +0200 Subject: [PATCH 95/96] feat(filebrowser): support subdomain = false (#286) Co-authored-by: Muhammad Atif Ali --- filebrowser/README.md | 28 +++++++++++++++++++++------- filebrowser/main.test.ts | 28 ++++++++++++++++++++++++++++ filebrowser/main.tf | 24 ++++++++++++++++++++++-- filebrowser/run.sh | 3 +++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/filebrowser/README.md b/filebrowser/README.md index 2881376..50b503a 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -13,9 +13,10 @@ A file browser for your workspace. ```tf module "filebrowser" { - source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.8" - agent_id = coder_agent.example.id + source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.8" + agent_id = coder_agent.example.id + agent_name = "main" } ``` @@ -27,10 +28,11 @@ module "filebrowser" { ```tf module "filebrowser" { - source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.8" - agent_id = coder_agent.example.id - folder = "/home/coder/project" + source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.8" + agent_id = coder_agent.example.id + agent_name = "main" + folder = "/home/coder/project" } ``` @@ -41,6 +43,18 @@ module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" version = "1.0.8" agent_id = coder_agent.example.id + agent_name = "main" database_path = ".config/filebrowser.db" } ``` + +### Serve from the same domain (no subdomain) + +```tf +module "filebrowser" { + source = "registry.coder.com/modules/filebrowser/coder" + agent_id = coder_agent.example.id + agent_name = "main" + subdomain = false +} +``` diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts index 79dd99d..ff6d045 100644 --- a/filebrowser/main.test.ts +++ b/filebrowser/main.test.ts @@ -11,11 +11,13 @@ describe("filebrowser", async () => { testRequiredVariables(import.meta.dir, { agent_id: "foo", + agent_name: "main", }); it("fails with wrong database_path", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + agent_name: "main", database_path: "nofb", }).catch((e) => { if (!e.message.startsWith("\nError: Invalid value for variable")) { @@ -27,6 +29,7 @@ describe("filebrowser", async () => { it("runs with default", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + agent_name: "main", }); const output = await executeScriptInContainer(state, "alpine"); expect(output.exitCode).toBe(0); @@ -48,6 +51,7 @@ describe("filebrowser", async () => { it("runs with database_path var", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + agent_name: "main", database_path: ".config/filebrowser.db", }); const output = await executeScriptInContainer(state, "alpine"); @@ -70,6 +74,7 @@ describe("filebrowser", async () => { it("runs with folder var", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + agent_name: "main", folder: "/home/coder/project", }); const output = await executeScriptInContainer(state, "alpine"); @@ -88,4 +93,27 @@ describe("filebrowser", async () => { "📝 Logs at /tmp/filebrowser.log", ]); }); + + it("runs with subdomain=false", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + agent_name: "main", + subdomain: false, + }); + const output = await executeScriptInContainer(state, "alpine"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "\u001B[0;1mInstalling filebrowser ", + "", + "🥳 Installation complete! ", + "", + "👷 Starting filebrowser in background... ", + "", + "📂 Serving /root at http://localhost:13339 ", + "", + "Running 'filebrowser --noauth --root /root --port 13339' ", + "", + "📝 Logs at /tmp/filebrowser.log", + ]); + }); }); diff --git a/filebrowser/main.tf b/filebrowser/main.tf index a07072b..e6b88c6 100644 --- a/filebrowser/main.tf +++ b/filebrowser/main.tf @@ -14,6 +14,15 @@ variable "agent_id" { description = "The ID of a Coder agent." } +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +variable "agent_name" { + type = string + description = "The name of the main deployment. (Used to build the subpath for coder_app.)" +} + variable "database_path" { type = string description = "The path to the filebrowser database." @@ -58,6 +67,15 @@ variable "order" { default = null } +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = true +} + resource "coder_script" "filebrowser" { agent_id = var.agent_id display_name = "File Browser" @@ -67,7 +85,9 @@ resource "coder_script" "filebrowser" { PORT : var.port, FOLDER : var.folder, LOG_PATH : var.log_path, - DB_PATH : var.database_path + DB_PATH : var.database_path, + SUBDOMAIN : var.subdomain, + SERVER_BASE_PATH : var.subdomain ? "" : format("/@%s/%s.%s/apps/filebrowser", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name), }) run_on_start = true } @@ -78,7 +98,7 @@ resource "coder_app" "filebrowser" { display_name = "File Browser" url = "http://localhost:${var.port}" icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" - subdomain = true + subdomain = var.subdomain share = var.share order = var.order } diff --git a/filebrowser/run.sh b/filebrowser/run.sh index 8744edb..22f13ed 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -17,6 +17,9 @@ if [ "${DB_PATH}" != "filebrowser.db" ]; then DB_FLAG=" -d ${DB_PATH}" fi +# set baseurl to be able to run if sudomain=false; if subdomain=true the SERVER_BASE_PATH value will be "" +filebrowser config set --baseurl "${SERVER_BASE_PATH}" > ${LOG_PATH} 2>&1 + printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n" printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n" From b51932d7ac15ebbdff700b9072e362f94eeac269 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 20 Sep 2024 05:00:06 -0700 Subject: [PATCH 96/96] feat(dotfiles): Add an optional coder_app to update dotfiles on-demand (#280) Co-authored-by: Chris Golden <551285+cirego@users.noreply.github.com> Co-authored-by: Michael Smith --- dotfiles/main.tf | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/dotfiles/main.tf b/dotfiles/main.tf index bfb67e4..9bc3735 100644 --- a/dotfiles/main.tf +++ b/dotfiles/main.tf @@ -39,9 +39,14 @@ variable "coder_parameter_order" { default = null } -data "coder_parameter" "dotfiles_uri" { - count = var.dotfiles_uri == null ? 1 : 0 +variable "manual_update" { + type = bool + description = "If true, this adds a button to workspace page to refresh dotfiles on demand." + default = false +} +data "coder_parameter" "dotfiles_uri" { + count = var.dotfiles_uri == null ? 1 : 0 type = "string" name = "dotfiles_uri" display_name = "Dotfiles URL" @@ -68,6 +73,18 @@ resource "coder_script" "dotfiles" { run_on_start = true } +resource "coder_app" "dotfiles" { + count = var.manual_update ? 1 : 0 + agent_id = var.agent_id + display_name = "Refresh Dotfiles" + slug = "dotfiles" + icon = "/icon/dotfiles.svg" + command = templatefile("${path.module}/run.sh", { + DOTFILES_URI : local.dotfiles_uri, + DOTFILES_USER : local.user + }) +} + output "dotfiles_uri" { description = "Dotfiles URI" value = local.dotfiles_uri