Compare commits
126 Commits
web-rdp-cl
...
v1.0.18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86038f8d37 | ||
|
|
120a0e342e | ||
|
|
b51932d7ac | ||
|
|
834ffde032 | ||
|
|
831f64da56 | ||
|
|
236022f870 | ||
|
|
4c45d69994 | ||
|
|
310d0262bd | ||
|
|
f446fbd667 | ||
|
|
982c75e86f | ||
|
|
523ad9fe23 | ||
|
|
096cd214ce | ||
|
|
6a87fd18e5 | ||
|
|
fa4b84e8d1 | ||
|
|
7e0eacf1f4 | ||
|
|
cbe48aa072 | ||
|
|
89bb023fa5 | ||
|
|
66472b0105 | ||
|
|
cd010baac8 | ||
|
|
f7fa145855 | ||
|
|
f7f9c8b7ef | ||
|
|
889186d553 | ||
|
|
352577b833 | ||
|
|
4e59ecc606 | ||
|
|
a40f2b86c3 | ||
|
|
a2c29ace0a | ||
|
|
da4a561cb5 | ||
|
|
d77ad8ac63 | ||
|
|
b1f81afa7f | ||
|
|
883741244b | ||
|
|
c3eee866d1 | ||
|
|
bf175a1247 | ||
|
|
8fd54e0e78 | ||
|
|
e8ee02c044 | ||
|
|
aebdc9b434 | ||
|
|
d98bfcb20b | ||
|
|
894e507bb3 | ||
|
|
3f8f6181e0 | ||
|
|
b23d85327c | ||
|
|
a8580fe6b9 | ||
|
|
49f060549e | ||
|
|
b4153a6aaa | ||
|
|
13a8877791 | ||
|
|
fd2f91c043 | ||
|
|
c59eb0c0cc | ||
|
|
a381c3ee29 | ||
|
|
d9d1be08a3 | ||
|
|
7a8483d816 | ||
|
|
ec2c8edfb2 | ||
|
|
78f91a542a | ||
|
|
78c948094d | ||
|
|
16f96d3693 | ||
|
|
8262b29063 | ||
|
|
4ab72575ac | ||
|
|
f369697112 | ||
|
|
f82c7fd7a1 | ||
|
|
05a20a9e1f | ||
|
|
90e15cd90c | ||
|
|
5869eb86d4 | ||
|
|
25c90001f4 | ||
|
|
6409ee2bba | ||
|
|
7d366ff92a | ||
|
|
45456ab394 | ||
|
|
c652dbe320 | ||
|
|
4021d856ba | ||
|
|
72eaf8a9e1 | ||
|
|
249cb2fe9e | ||
|
|
49cff4b2aa | ||
|
|
c6b457e7fe | ||
|
|
beaa33b682 | ||
|
|
0d7bc37f9c | ||
|
|
dcd605c52e | ||
|
|
f5d41520cf | ||
|
|
cd0c730c95 | ||
|
|
873207fddf | ||
|
|
282e1f8c57 | ||
|
|
c068082e6b | ||
|
|
85e73c2071 | ||
|
|
4bdb428244 | ||
|
|
daed803530 | ||
|
|
a239212f0b | ||
|
|
67fef297da | ||
|
|
aced7547bc | ||
|
|
36fa871e7b | ||
|
|
46bf422d61 | ||
|
|
180e10c3ee | ||
|
|
a45706ad3a | ||
|
|
5030fcb988 | ||
|
|
cff60c4a7e | ||
|
|
5a33af28ac | ||
|
|
428f386c4c | ||
|
|
2e43788584 | ||
|
|
e8ce194ff7 | ||
|
|
1273378ca8 | ||
|
|
edc163b5f2 | ||
|
|
c9e418aaf5 | ||
|
|
9062b4c004 | ||
|
|
b2e87ef038 | ||
|
|
d4db52017d | ||
|
|
c36f4e03d7 | ||
|
|
443485a2d7 | ||
|
|
b686f2dbd5 | ||
|
|
76c60e9971 | ||
|
|
b0d6224e23 | ||
|
|
c50c4259d9 | ||
|
|
5f312ced5e | ||
|
|
fd985bedac | ||
|
|
b0c14be846 | ||
|
|
18efe83b89 | ||
|
|
33dbae6ea0 | ||
|
|
f14e6838e4 | ||
|
|
2a30982d1a | ||
|
|
47e995f636 | ||
|
|
56fdf096c1 | ||
|
|
49df203bd6 | ||
|
|
8766c670e6 | ||
|
|
43304e5d4e | ||
|
|
d8f71e4571 | ||
|
|
d8102e62ec | ||
|
|
ed16ba59a9 | ||
|
|
a8c659ad6f | ||
|
|
c4df384f4b | ||
|
|
892174da7c | ||
|
|
24e50e2bbb | ||
|
|
dfe69f25ce | ||
|
|
e8f6578ece |
19
.icons/airflow.svg
Normal file
19
.icons/airflow.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
1
.icons/cursor.svg
Normal file
1
.icons/cursor.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.5 MiB |
1
.icons/github.svg
Normal file
1
.icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 960 B |
BIN
.images/airflow.png
Normal file
BIN
.images/airflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
@@ -1,22 +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
|
||||
./new.sh MODULE_NAME
|
||||
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 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.
|
||||
|
||||
Reference existing `*.test.ts` files for implementation.
|
||||
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 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 '<module>'
|
||||
```
|
||||
|
||||
Or run all tests by running this command:
|
||||
|
||||
```shell
|
||||
$ bun test
|
||||
```
|
||||
|
||||
You can test a module locally by updating the source as follows
|
||||
|
||||
```tf
|
||||
@@ -25,4 +50,25 @@ module "example" {
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.
|
||||
## Releases
|
||||
|
||||
> [!WARNING]
|
||||
> 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<major>.<minor>.<patch>` (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 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.
|
||||
|
||||
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. This data should be updated almost immediately after a new release, but other changes will take some time to propagate.
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
Modules
|
||||
</h1>
|
||||
|
||||
[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)
|
||||
|
||||
[](https://discord.gg/coder)
|
||||
[](./LICENSE)
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
|
||||
23
apache-airflow/README.md
Normal file
23
apache-airflow/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
display_name: airflow
|
||||
description: A module that adds Apache Airflow in your Coder template
|
||||
icon: ../.icons/airflow.svg
|
||||
maintainer_github: coder
|
||||
partner_github: nataindata
|
||||
verified: true
|
||||
tags: [airflow, idea, web, helper]
|
||||
---
|
||||
|
||||
# airflow
|
||||
|
||||
A module that adds Apache Airflow in your Coder template.
|
||||
|
||||
```tf
|
||||
module "airflow" {
|
||||
source = "registry.coder.com/modules/apache-airflow/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
65
apache-airflow/main.tf
Normal file
65
apache-airflow/main.tf
Normal file
@@ -0,0 +1,65 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log airflow to."
|
||||
default = "/tmp/airflow.log"
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run airflow on."
|
||||
default = 8080
|
||||
}
|
||||
|
||||
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 "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "airflow" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "airflow"
|
||||
icon = "/icon/apache-guacamole.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
LOG_PATH : var.log_path,
|
||||
PORT : var.port
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "airflow" {
|
||||
agent_id = var.agent_id
|
||||
slug = "airflow"
|
||||
display_name = "airflow"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/apache-guacamole.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
}
|
||||
19
apache-airflow/run.sh
Normal file
19
apache-airflow/run.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
PATH=$PATH:~/.local/bin
|
||||
pip install --upgrade apache-airflow
|
||||
|
||||
filename=~/airflow/airflow.db
|
||||
if ! [ -f $filename ] || ! [ -s $filename ]; then
|
||||
airflow db init
|
||||
fi
|
||||
|
||||
export AIRFLOW__CORE__LOAD_EXAMPLES=false
|
||||
|
||||
airflow webserver > ${LOG_PATH} 2>&1 &
|
||||
|
||||
airflow scheduler >> /tmp/airflow_scheduler.log 2>&1 &
|
||||
|
||||
airflow users create -u admin -p admin -r Admin -e admin@admin.com -f Coder -l User
|
||||
@@ -17,7 +17,7 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.12"
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Change the display name and icon for a region using the corresponding maps:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.12"
|
||||
default = "ap-south-1"
|
||||
|
||||
custom_names = {
|
||||
@@ -63,7 +63,7 @@ Hide the Asia Pacific regions Seoul and Osaka:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.12"
|
||||
exclude = ["ap-northeast-2", "ap-northeast-3"]
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,13 @@ describe("aws-region", async () => {
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("us-west-2");
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,12 @@ variable "exclude" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# This is a static list because the regions don't change _that_
|
||||
# frequently and including the `aws_regions` data source requires
|
||||
@@ -176,6 +182,7 @@ data "coder_parameter" "region" {
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
dynamic "option" {
|
||||
for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) }
|
||||
|
||||
@@ -14,7 +14,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele
|
||||
```tf
|
||||
module "azure_region" {
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "eastus"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Change the display name and icon for a region using the corresponding maps:
|
||||
```tf
|
||||
module "azure-region" {
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
custom_names = {
|
||||
"australia" : "Go Australia!"
|
||||
}
|
||||
@@ -57,7 +57,7 @@ Hide all regions in Australia except australiacentral:
|
||||
```tf
|
||||
module "azure-region" {
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
exclude = [
|
||||
"australia",
|
||||
"australiacentral2",
|
||||
|
||||
@@ -22,4 +22,13 @@ describe("azure-region", async () => {
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("westus");
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,12 @@ variable "exclude" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# Note: Options are limited to 64 regions, some redundant regions have been removed.
|
||||
all_regions = {
|
||||
@@ -309,6 +315,7 @@ data "coder_parameter" "region" {
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
icon = "/icon/azure.png"
|
||||
dynamic "option" {
|
||||
|
||||
@@ -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.10"
|
||||
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.10"
|
||||
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.10"
|
||||
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.10"
|
||||
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.10"
|
||||
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.10"
|
||||
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.10"
|
||||
version = "1.0.17"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -95,6 +95,33 @@ variable "use_cached" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "use_cached_extensions" {
|
||||
type = bool
|
||||
description = "Uses cached copy of extensions, otherwise do a forced upgrade"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extensions_dir" {
|
||||
type = string
|
||||
description = "Override the directory to store extensions in."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "auto_install_extensions" {
|
||||
type = bool
|
||||
description = "Automatically install recommended extensions when code-server starts."
|
||||
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"
|
||||
@@ -110,6 +137,10 @@ resource "coder_script" "code-server" {
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
USE_CACHED_EXTENSIONS : var.use_cached_extensions,
|
||||
EXTENSIONS_DIR : var.extensions_dir,
|
||||
FOLDER : var.folder,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -132,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
|
||||
|
||||
|
||||
@@ -6,10 +6,17 @@ CODE='\033[36;40;1m'
|
||||
RESET='\033[0m'
|
||||
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
|
||||
|
||||
# Set extension directory
|
||||
EXTENSION_ARG=""
|
||||
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||
mkdir -p "${EXTENSIONS_DIR}"
|
||||
fi
|
||||
|
||||
function run_code_server() {
|
||||
echo "👷 Running code-server in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
$CODE_SERVER --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
|
||||
$CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
@@ -19,36 +26,53 @@ if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
|
||||
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
||||
fi
|
||||
|
||||
# Check if code-server is already installed for offline or cached mode
|
||||
if [ -f "$CODE_SERVER" ]; then
|
||||
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
||||
# Check if code-server is already installed for offline
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
if [ -f "$CODE_SERVER" ]; then
|
||||
echo "🥳 Found a copy of code-server"
|
||||
run_code_server
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# Offline mode always expects a copy of code-server to be present
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
# Offline mode always expects a copy of code-server to be present
|
||||
echo "Failed to find a copy of code-server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "$${BOLD}Installing code-server!\n"
|
||||
# If there is no cached install OR we don't want to use a cached install
|
||||
if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then
|
||||
printf "$${BOLD}Installing code-server!\n"
|
||||
|
||||
ARGS=(
|
||||
"--method=standalone"
|
||||
"--prefix=${INSTALL_PREFIX}"
|
||||
)
|
||||
if [ -n "${VERSION}" ]; then
|
||||
ARGS+=("--version=${VERSION}")
|
||||
ARGS=(
|
||||
"--method=standalone"
|
||||
"--prefix=${INSTALL_PREFIX}"
|
||||
)
|
||||
if [ -n "${VERSION}" ]; then
|
||||
ARGS+=("--version=${VERSION}")
|
||||
fi
|
||||
|
||||
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install code-server: $output"
|
||||
exit 1
|
||||
fi
|
||||
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
||||
fi
|
||||
|
||||
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install code-server: $output"
|
||||
exit 1
|
||||
fi
|
||||
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
||||
# Get the list of installed extensions...
|
||||
LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG)
|
||||
readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS"
|
||||
function extension_installed() {
|
||||
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then
|
||||
return 1
|
||||
fi
|
||||
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
|
||||
if [ "$_extension" == "$1" ]; then
|
||||
echo "Extension $1 was already installed."
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install each extension...
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
@@ -56,12 +80,38 @@ for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
fi
|
||||
if extension_installed "$extension"; then
|
||||
continue
|
||||
fi
|
||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||
output=$($CODE_SERVER --install-extension "$extension")
|
||||
output=$($CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install extension: $extension: $output"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
fi
|
||||
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
|
||||
for extension in $extensions; do
|
||||
if extension_installed "$extension"; then
|
||||
continue
|
||||
fi
|
||||
$CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
run_code_server
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically logs the user into Coder when creating their workspace.
|
||||
```tf
|
||||
module "coder-login" {
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,12 @@ variable "agent_id" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_script" "coder-login" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_USER_TOKEN : data.coder_workspace.me.owner_session_token,
|
||||
CODER_USER_TOKEN : data.coder_workspace_owner.me.session_token,
|
||||
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
|
||||
})
|
||||
display_name = "Coder Login"
|
||||
|
||||
35
cursor/README.md
Normal file
35
cursor/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
display_name: Cursor IDE
|
||||
description: Add a one-click button to launch Cursor IDE
|
||||
icon: ../.icons/cursor.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, cursor, helper]
|
||||
---
|
||||
|
||||
# Cursor IDE
|
||||
|
||||
Add a button to open any workspace with a single click in Cursor IDE.
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder).
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
source = "registry.coder.com/modules/cursor/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
source = "registry.coder.com/modules/cursor/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
89
cursor/main.test.ts
Normal file
89
cursor/main.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
executeScriptInContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "../test";
|
||||
|
||||
describe("cursor", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.cursor_url.value).toBe(
|
||||
"cursor://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.cursor_url.value).toBe(
|
||||
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.cursor_url.value).toBe(
|
||||
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
});
|
||||
expect(state.outputs.cursor_url.value).toBe(
|
||||
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.cursor_url.value).toBe(
|
||||
"cursor://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
62
cursor/main.tf
Normal file
62
cursor/main.tf
Normal file
@@ -0,0 +1,62 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Cursor IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/cursor.svg"
|
||||
slug = "cursor"
|
||||
display_name = "Cursor IDE"
|
||||
order = var.order
|
||||
url = join("", [
|
||||
"cursor://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
}
|
||||
|
||||
output "cursor_url" {
|
||||
value = coder_app.cursor.url
|
||||
description = "Cursor IDE Desktop URL."
|
||||
}
|
||||
@@ -9,12 +9,70 @@ tags: [helper]
|
||||
|
||||
# Dotfiles
|
||||
|
||||
Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io)! Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
|
||||
Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io).
|
||||
|
||||
This will prompt the user for their dotfiles repository URL on template creation using a `coder_parameter`.
|
||||
|
||||
Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
|
||||
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Apply dotfiles as the current user
|
||||
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
### Apply dotfiles as another user (only works if sudo is passwordless)
|
||||
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
}
|
||||
```
|
||||
|
||||
### Apply the same dotfiles as the current user and root (the root dotfiles can only be applied if sudo is passwordless)
|
||||
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "dotfiles-root" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||
}
|
||||
```
|
||||
|
||||
## Setting a default dotfiles repository
|
||||
|
||||
You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable:
|
||||
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -18,4 +18,23 @@ describe("dotfiles", async () => {
|
||||
});
|
||||
expect(state.outputs.dotfiles_uri.value).toBe("");
|
||||
});
|
||||
|
||||
it("set a default dotfiles_uri", async () => {
|
||||
const default_dotfiles_uri = "foo";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
default_dotfiles_uri,
|
||||
});
|
||||
expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri);
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(2);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,30 +14,78 @@ variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "default_dotfiles_uri" {
|
||||
type = string
|
||||
description = "The default dotfiles URI if the workspace user does not provide one"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "dotfiles_uri" {
|
||||
type = string
|
||||
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
|
||||
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "user" {
|
||||
type = string
|
||||
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
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 (optional)"
|
||||
default = ""
|
||||
display_name = "Dotfiles URL"
|
||||
order = var.coder_parameter_order
|
||||
default = var.default_dotfiles_uri
|
||||
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||
mutable = true
|
||||
icon = "/icon/dotfiles.svg"
|
||||
}
|
||||
|
||||
resource "coder_script" "personalize" {
|
||||
agent_id = var.agent_id
|
||||
script = <<-EOT
|
||||
DOTFILES_URI="${data.coder_parameter.dotfiles_uri.value}"
|
||||
if [ -n "$${DOTFILES_URI// }" ]; then
|
||||
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.dotfiles.log
|
||||
fi
|
||||
EOT
|
||||
locals {
|
||||
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
|
||||
user = var.user != null ? var.user : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "dotfiles" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOTFILES_URI : local.dotfiles_uri,
|
||||
DOTFILES_USER : local.user
|
||||
})
|
||||
display_name = "Dotfiles"
|
||||
icon = "/icon/dotfiles.svg"
|
||||
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 = data.coder_parameter.dotfiles_uri.value
|
||||
}
|
||||
value = local.dotfiles_uri
|
||||
}
|
||||
|
||||
23
dotfiles/run.sh
Normal file
23
dotfiles/run.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
DOTFILES_URI="${DOTFILES_URI}"
|
||||
DOTFILES_USER="${DOTFILES_USER}"
|
||||
|
||||
if [ -n "$${DOTFILES_URI// }" ]; then
|
||||
if [ -z "$DOTFILES_USER" ]; then
|
||||
DOTFILES_USER="$USER"
|
||||
fi
|
||||
|
||||
echo "✨ Applying dotfiles for user $DOTFILES_USER"
|
||||
|
||||
if [ "$DOTFILES_USER" = "$USER" ]; then
|
||||
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
|
||||
else
|
||||
# The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280
|
||||
# eval echo ~coder -> "/home/coder"
|
||||
# eval echo ~root -> "/root"
|
||||
|
||||
CODER_BIN=$(which coder)
|
||||
DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER")
|
||||
sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log"
|
||||
fi
|
||||
fi
|
||||
@@ -17,7 +17,7 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "standard.medium"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ Change the display name a type using the corresponding maps:
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "standard.medium"
|
||||
|
||||
custom_names = {
|
||||
@@ -79,7 +79,7 @@ Show only gpu1 types
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "gpu.large"
|
||||
type_category = ["gpu"]
|
||||
exclude = [
|
||||
|
||||
@@ -31,4 +31,13 @@ describe("exoscale-instance-type", async () => {
|
||||
});
|
||||
}).toThrow('default value "gpu3.huge" must be defined as one of options');
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,12 @@ variable "exclude" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# https://www.exoscale.com/pricing/
|
||||
|
||||
@@ -257,6 +263,7 @@ data "coder_parameter" "instance_type" {
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
dynamic "option" {
|
||||
for_each = [for k, v in concat(
|
||||
|
||||
@@ -17,7 +17,7 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "ch-dk-2"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ Change the display name and icon for a zone using the corresponding maps:
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = "at-vie-1"
|
||||
|
||||
custom_names = {
|
||||
@@ -76,7 +76,7 @@ Hide the Switzerland zones Geneva and Zurich
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
exclude = ["ch-gva-2", "ch-dk-2"]
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,13 @@ describe("exoscale-zone", async () => {
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("at-vie-1");
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,11 @@ variable "exclude" {
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# This is a static list because the zones don't change _that_
|
||||
@@ -94,6 +99,7 @@ data "coder_parameter" "zone" {
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
dynamic "option" {
|
||||
for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -14,7 +14,7 @@ This module adds Google Cloud Platform regions to your Coder template.
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
regions = ["us", "europe"]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support,
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
default = ["us-west1-a"]
|
||||
regions = ["us-west1"]
|
||||
gpu_only = false
|
||||
@@ -50,7 +50,7 @@ resource "google_compute_instance" "example" {
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
regions = ["europe-west"]
|
||||
single_zone_per_region = false
|
||||
}
|
||||
@@ -65,7 +65,7 @@ resource "google_compute_instance" "example" {
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
regions = ["us", "europe"]
|
||||
gpu_only = true
|
||||
single_zone_per_region = true
|
||||
|
||||
@@ -40,4 +40,13 @@ describe("gcp-region", async () => {
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("us-west2-b");
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,12 @@ variable "single_zone_per_region" {
|
||||
type = bool
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
zones = {
|
||||
# US Central
|
||||
@@ -715,6 +721,7 @@ data "coder_parameter" "region" {
|
||||
icon = "/icon/gcp.png"
|
||||
mutable = var.mutable
|
||||
default = var.default != null && var.default != "" && (!var.gpu_only || try(local.zones[var.default].gpu, false)) ? var.default : null
|
||||
order = var.coder_parameter_order
|
||||
dynamic "option" {
|
||||
for_each = {
|
||||
for k, v in local.zones : k => v
|
||||
|
||||
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
```tf
|
||||
module "git-clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -27,7 +27,7 @@ module "git-clone" {
|
||||
```tf
|
||||
module "git-clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -41,7 +41,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
```tf
|
||||
module "git-clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -50,3 +50,123 @@ data "coder_git_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
```
|
||||
|
||||
## GitHub clone with branch name
|
||||
|
||||
To GitHub clone with a specific branch like `feat/example`
|
||||
|
||||
```tf
|
||||
# Prompt the user for the git repo URL
|
||||
data "coder_parameter" "git_repo" {
|
||||
name = "git_repo"
|
||||
display_name = "Git repository"
|
||||
default = "https://github.com/coder/coder/tree/feat/example"
|
||||
}
|
||||
|
||||
# Clone the repository for branch `feat/example`
|
||||
module "git_clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
|
||||
# Create a code-server instance for the cloned repository
|
||||
module "code-server" {
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
order = 1
|
||||
folder = "/home/${local.username}/${module.git_clone.folder_name}"
|
||||
}
|
||||
|
||||
# Create a Coder app for the website
|
||||
resource "coder_app" "website" {
|
||||
agent_id = coder_agent.example.id
|
||||
order = 2
|
||||
slug = "website"
|
||||
external = true
|
||||
display_name = module.git_clone.folder_name
|
||||
url = module.git_clone.web_url
|
||||
icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg"
|
||||
count = module.git_clone.web_url != "" ? 1 : 0
|
||||
}
|
||||
```
|
||||
|
||||
Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com`
|
||||
|
||||
```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.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://github.example.com/" = {
|
||||
provider = "github"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GitLab clone with branch name
|
||||
|
||||
To GitLab clone with a specific branch like `feat/example`
|
||||
|
||||
```tf
|
||||
module "git-clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
```
|
||||
|
||||
Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
|
||||
```tf
|
||||
module "git-clone" {
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.0.12"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://gitlab.example.com/" = {
|
||||
provider = "gitlab"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Git clone with branch_name set
|
||||
|
||||
Alternatively, you can set the `branch_name` attribute to clone a specific branch.
|
||||
|
||||
For example, to clone the `feat/example` branch:
|
||||
|
||||
```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"
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,4 +36,212 @@ describe("git-clone", async () => {
|
||||
"Cloning fake-url to ~/fake-url...",
|
||||
]);
|
||||
});
|
||||
|
||||
it("repo_dir should match repo name for https", async () => {
|
||||
const url = "https://github.com/coder/coder.git";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
|
||||
expect(state.outputs.folder_name.value).toEqual("coder");
|
||||
expect(state.outputs.clone_url.value).toEqual(url);
|
||||
expect(state.outputs.web_url.value).toEqual(url);
|
||||
expect(state.outputs.branch_name.value).toEqual("");
|
||||
});
|
||||
|
||||
it("repo_dir should match repo name for https without .git", async () => {
|
||||
const url = "https://github.com/coder/coder";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
|
||||
expect(state.outputs.clone_url.value).toEqual(url);
|
||||
expect(state.outputs.web_url.value).toEqual(url);
|
||||
expect(state.outputs.branch_name.value).toEqual("");
|
||||
});
|
||||
|
||||
it("repo_dir should match repo name for ssh", async () => {
|
||||
const url = "git@github.com:coder/coder.git";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
|
||||
expect(state.outputs.git_provider.value).toEqual("");
|
||||
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("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",
|
||||
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch?ref_type=heads",
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log");
|
||||
expect(state.outputs.folder_name.value).toEqual("repo-tests.log");
|
||||
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
|
||||
expect(state.outputs.clone_url.value).toEqual(https_url);
|
||||
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||
expect(state.outputs.branch_name.value).toEqual("feat/branch");
|
||||
});
|
||||
|
||||
it("branch_name should not include fragments", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch#name",
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
|
||||
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
|
||||
expect(state.outputs.clone_url.value).toEqual(https_url);
|
||||
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||
expect(state.outputs.branch_name.value).toEqual("feat/branch");
|
||||
});
|
||||
|
||||
it("gitlab url with branch should match", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch",
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
|
||||
expect(state.outputs.git_provider.value).toEqual("gitlab");
|
||||
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
|
||||
expect(state.outputs.clone_url.value).toEqual(https_url);
|
||||
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||
expect(state.outputs.branch_name.value).toEqual("feat/branch");
|
||||
});
|
||||
|
||||
it("github url with branch should match", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch",
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
|
||||
expect(state.outputs.git_provider.value).toEqual("github");
|
||||
const https_url = "https://github.com/michaelbrewer/repo-tests.log";
|
||||
expect(state.outputs.clone_url.value).toEqual(https_url);
|
||||
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||
expect(state.outputs.branch_name.value).toEqual("feat/branch");
|
||||
});
|
||||
|
||||
it("self-host git url with branch should match", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url: "https://git.example.com/example/project/-/tree/feat/example",
|
||||
git_providers: `
|
||||
{
|
||||
"https://git.example.com/" = {
|
||||
provider = "gitlab"
|
||||
}
|
||||
}`,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/project");
|
||||
expect(state.outputs.git_provider.value).toEqual("gitlab");
|
||||
const https_url = "https://git.example.com/example/project";
|
||||
expect(state.outputs.clone_url.value).toEqual(https_url);
|
||||
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||
expect(state.outputs.branch_name.value).toEqual("feat/example");
|
||||
});
|
||||
|
||||
it("handle unsupported git provider configuration", async () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "foo",
|
||||
git_providers: `
|
||||
{
|
||||
"https://git.example.com/" = {
|
||||
provider = "bitbucket"
|
||||
}
|
||||
}`,
|
||||
});
|
||||
};
|
||||
expect(t).toThrow('Allowed values for provider are "github" or "gitlab".');
|
||||
});
|
||||
|
||||
it("handle unknown git provider url", async () => {
|
||||
const url = "https://git.unknown.com/coder/coder";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
base_dir: "/tmp",
|
||||
url,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
|
||||
expect(state.outputs.clone_url.value).toEqual(url);
|
||||
expect(state.outputs.web_url.value).toEqual(url);
|
||||
expect(state.outputs.branch_name.value).toEqual("");
|
||||
});
|
||||
|
||||
it("runs with github clone with switch to feat/branch", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch",
|
||||
});
|
||||
const output = await executeScriptInContainer(state, "alpine/git");
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(output.stdout).toEqual([
|
||||
"Creating directory ~/repo-tests.log...",
|
||||
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs with gitlab clone with switch to feat/branch", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch",
|
||||
});
|
||||
const output = await executeScriptInContainer(state, "alpine/git");
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(output.stdout).toEqual([
|
||||
"Creating directory ~/repo-tests.log...",
|
||||
"Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs with github clone with branch_name set to feat/branch", async () => {
|
||||
const url = "https://github.com/michaelbrewer/repo-tests.log";
|
||||
const branch_name = "feat/branch";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url,
|
||||
branch_name,
|
||||
});
|
||||
expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log");
|
||||
expect(state.outputs.clone_url.value).toEqual(url);
|
||||
expect(state.outputs.web_url.value).toEqual(url);
|
||||
expect(state.outputs.branch_name.value).toEqual(branch_name);
|
||||
|
||||
const output = await executeScriptInContainer(state, "alpine/git");
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(output.stdout).toEqual([
|
||||
"Creating directory ~/repo-tests.log...",
|
||||
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,8 +25,56 @@ variable "agent_id" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "git_providers" {
|
||||
type = map(object({
|
||||
provider = string
|
||||
}))
|
||||
description = "A mapping of URLs to their git provider."
|
||||
default = {
|
||||
"https://github.com/" = {
|
||||
provider = "github"
|
||||
},
|
||||
"https://gitlab.com/" = {
|
||||
provider = "gitlab"
|
||||
},
|
||||
}
|
||||
validation {
|
||||
error_message = "Allowed values for provider are \"github\" or \"gitlab\"."
|
||||
condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)])
|
||||
}
|
||||
}
|
||||
|
||||
variable "branch_name" {
|
||||
description = "The branch name to clone. If not provided, the default branch will be cloned."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "folder_name" {
|
||||
description = "The destination folder to clone the repository into."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
locals {
|
||||
clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")])
|
||||
# Remove query parameters and fragments from the URL
|
||||
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
||||
|
||||
# Find the git provider based on the URL and determine the tree path
|
||||
provider_key = try(one([for key in keys(var.git_providers) : key if startswith(local.url, key)]), null)
|
||||
provider = try(lookup(var.git_providers, local.provider_key).provider, "")
|
||||
tree_path = local.provider == "gitlab" ? "/-/tree/" : local.provider == "github" ? "/tree/" : ""
|
||||
|
||||
# Remove tree and branch name from the URL
|
||||
clone_url = var.branch_name == "" && local.tree_path != "" ? replace(local.url, "/${local.tree_path}.*/", "") : local.url
|
||||
# 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 = 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
|
||||
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
|
||||
}
|
||||
|
||||
output "repo_dir" {
|
||||
@@ -34,11 +82,37 @@ output "repo_dir" {
|
||||
description = "Full path of cloned repo directory"
|
||||
}
|
||||
|
||||
output "git_provider" {
|
||||
value = local.provider
|
||||
description = "The git provider of the repository"
|
||||
}
|
||||
|
||||
output "folder_name" {
|
||||
value = local.folder_name
|
||||
description = "The name of the folder that will be created"
|
||||
}
|
||||
|
||||
output "clone_url" {
|
||||
value = local.clone_url
|
||||
description = "The exact Git repository URL that will be cloned"
|
||||
}
|
||||
|
||||
output "web_url" {
|
||||
value = local.web_url
|
||||
description = "Git https repository URL (may be invalid for unsupported providers)"
|
||||
}
|
||||
|
||||
output "branch_name" {
|
||||
value = local.branch_name
|
||||
description = "Git branch name (may be empty)"
|
||||
}
|
||||
|
||||
resource "coder_script" "git_clone" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CLONE_PATH = local.clone_path
|
||||
REPO_URL : var.url,
|
||||
CLONE_PATH = local.clone_path,
|
||||
REPO_URL : local.clone_url,
|
||||
BRANCH_NAME : local.branch_name,
|
||||
})
|
||||
display_name = "Git Clone"
|
||||
icon = "/icon/git.svg"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
REPO_URL="${REPO_URL}"
|
||||
CLONE_PATH="${CLONE_PATH}"
|
||||
BRANCH_NAME="${BRANCH_NAME}"
|
||||
# Expand home if it's specified!
|
||||
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
|
||||
|
||||
@@ -33,8 +34,13 @@ fi
|
||||
# Check if the directory is empty
|
||||
# and if it is, clone the repo, otherwise skip cloning
|
||||
if [ -z "$(ls -A "$CLONE_PATH")" ]; then
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH..."
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
if [ -z "$BRANCH_NAME" ]; then
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH..."
|
||||
git clone "$REPO_URL" "$CLONE_PATH"
|
||||
else
|
||||
echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..."
|
||||
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||
fi
|
||||
else
|
||||
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
|
||||
exit 0
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
display_name: Git commit signing
|
||||
description: Configures Git to sign commits using your Coder SSH key
|
||||
icon: ../.icons/git.svg
|
||||
maintainer_github: phorcys420
|
||||
verified: false
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, git]
|
||||
---
|
||||
|
||||
@@ -19,7 +19,7 @@ This module has a chance of conflicting with the user's dotfiles / the personali
|
||||
```tf
|
||||
module "git-commit-signing" {
|
||||
source = "registry.coder.com/modules/git-commit-signing/coder"
|
||||
version = "1.0.9"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@ Runs a script that updates git credentials in the workspace to match the user's
|
||||
```tf
|
||||
module "git-config" {
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -28,7 +28,7 @@ TODO: Add screenshot
|
||||
```tf
|
||||
module "git-config" {
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
allow_email_change = true
|
||||
}
|
||||
@@ -41,7 +41,7 @@ TODO: Add screenshot
|
||||
```tf
|
||||
module "git-config" {
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
allow_username_change = false
|
||||
allow_email_change = false
|
||||
|
||||
127
git-config/main.test.ts
Normal file
127
git-config/main.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "../test";
|
||||
|
||||
describe("git-config", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("can run apply allow_username_change and allow_email_change disabled", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
allow_username_change: "false",
|
||||
allow_email_change: "false",
|
||||
});
|
||||
|
||||
const resources = state.resources;
|
||||
expect(resources).toHaveLength(6);
|
||||
expect(resources).toMatchObject([
|
||||
{ type: "coder_workspace", name: "me" },
|
||||
{ type: "coder_workspace_owner", name: "me" },
|
||||
{ type: "coder_env", name: "git_author_email" },
|
||||
{ type: "coder_env", name: "git_author_name" },
|
||||
{ type: "coder_env", name: "git_commmiter_email" },
|
||||
{ type: "coder_env", name: "git_commmiter_name" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("can run apply allow_email_change enabled", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
allow_email_change: "true",
|
||||
});
|
||||
|
||||
const resources = state.resources;
|
||||
expect(resources).toHaveLength(8);
|
||||
expect(resources).toMatchObject([
|
||||
{ type: "coder_parameter", name: "user_email" },
|
||||
{ type: "coder_parameter", name: "username" },
|
||||
{ type: "coder_workspace", name: "me" },
|
||||
{ type: "coder_workspace_owner", name: "me" },
|
||||
{ type: "coder_env", name: "git_author_email" },
|
||||
{ type: "coder_env", name: "git_author_name" },
|
||||
{ type: "coder_env", name: "git_commmiter_email" },
|
||||
{ type: "coder_env", name: "git_commmiter_name" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("can run apply allow_email_change enabled", async () => {
|
||||
const state = await runTerraformApply(
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
allow_username_change: "false",
|
||||
allow_email_change: "false",
|
||||
},
|
||||
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@email.com" },
|
||||
);
|
||||
|
||||
const resources = state.resources;
|
||||
expect(resources).toHaveLength(6);
|
||||
expect(resources).toMatchObject([
|
||||
{ type: "coder_workspace", name: "me" },
|
||||
{ type: "coder_workspace_owner", name: "me" },
|
||||
{ type: "coder_env", name: "git_author_email" },
|
||||
{ type: "coder_env", name: "git_author_name" },
|
||||
{ type: "coder_env", name: "git_commmiter_email" },
|
||||
{ type: "coder_env", name: "git_commmiter_name" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter for both fields", async () => {
|
||||
const order = 20;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
allow_username_change: "true",
|
||||
allow_email_change: "true",
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
const resources = state.resources;
|
||||
expect(resources).toHaveLength(8);
|
||||
expect(resources).toMatchObject([
|
||||
{ type: "coder_parameter", name: "user_email" },
|
||||
{ type: "coder_parameter", name: "username" },
|
||||
{ type: "coder_workspace", name: "me" },
|
||||
{ type: "coder_workspace_owner", name: "me" },
|
||||
{ type: "coder_env", name: "git_author_email" },
|
||||
{ type: "coder_env", name: "git_author_name" },
|
||||
{ type: "coder_env", name: "git_commmiter_email" },
|
||||
{ type: "coder_env", name: "git_commmiter_name" },
|
||||
]);
|
||||
// user_email order is the same as the order
|
||||
expect(resources[0].instances[0].attributes.order).toBe(order);
|
||||
// username order is incremented by 1
|
||||
// @ts-ignore: Object is possibly 'null'.
|
||||
expect(resources[1].instances[0]?.attributes.order).toBe(order + 1);
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter for just username", async () => {
|
||||
const order = 30;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
allow_email_change: "false",
|
||||
allow_username_change: "true",
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
const resources = state.resources;
|
||||
expect(resources).toHaveLength(7);
|
||||
expect(resources).toMatchObject([
|
||||
{ type: "coder_parameter", name: "username" },
|
||||
{ type: "coder_workspace", name: "me" },
|
||||
{ type: "coder_workspace_owner", name: "me" },
|
||||
{ type: "coder_env", name: "git_author_email" },
|
||||
{ type: "coder_env", name: "git_author_name" },
|
||||
{ type: "coder_env", name: "git_commmiter_email" },
|
||||
{ type: "coder_env", name: "git_commmiter_name" },
|
||||
]);
|
||||
// user_email was not created
|
||||
// username order is incremented by 1
|
||||
expect(resources[0].instances[0].attributes.order).toBe(order + 1);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.13"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,21 @@ variable "allow_email_change" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "user_email" {
|
||||
count = var.allow_email_change ? 1 : 0
|
||||
name = "user_email"
|
||||
type = "string"
|
||||
default = ""
|
||||
order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null
|
||||
description = "Git user.email to be used for commits. Leave empty to default to Coder user's email."
|
||||
display_name = "Git config user.email"
|
||||
mutable = true
|
||||
@@ -44,6 +51,7 @@ data "coder_parameter" "username" {
|
||||
name = "username"
|
||||
type = "string"
|
||||
default = ""
|
||||
order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null
|
||||
description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
|
||||
display_name = "Full Name for Git config"
|
||||
mutable = true
|
||||
@@ -52,23 +60,25 @@ data "coder_parameter" "username" {
|
||||
resource "coder_env" "git_author_name" {
|
||||
agent_id = var.agent_id
|
||||
name = "GIT_AUTHOR_NAME"
|
||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
}
|
||||
|
||||
resource "coder_env" "git_commmiter_name" {
|
||||
agent_id = var.agent_id
|
||||
name = "GIT_COMMITTER_NAME"
|
||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
}
|
||||
|
||||
resource "coder_env" "git_author_email" {
|
||||
agent_id = var.agent_id
|
||||
name = "GIT_AUTHOR_EMAIL"
|
||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email)
|
||||
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
|
||||
}
|
||||
|
||||
resource "coder_env" "git_commmiter_email" {
|
||||
agent_id = var.agent_id
|
||||
name = "GIT_COMMITTER_EMAIL"
|
||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email)
|
||||
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
|
||||
}
|
||||
|
||||
53
github-upload-public-key/README.md
Normal file
53
github-upload-public-key/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
display_name: Github Upload Public Key
|
||||
description: Automates uploading Coder public key to Github so users don't have to.
|
||||
icon: ../.icons/github.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, git]
|
||||
---
|
||||
|
||||
# github-upload-public-key
|
||||
|
||||
Templates that utilize Github External Auth can automatically ensure that the Coder public key is uploaded to Github so that users can clone repositories without needing to upload the public key themselves.
|
||||
|
||||
```tf
|
||||
module "github-upload-public-key" {
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
# Requirements
|
||||
|
||||
This module requires `curl` and `jq` to be installed inside your workspace.
|
||||
|
||||
Github External Auth must be enabled in the workspace for this module to work. The Github app that is configured for external auth must have both read and write permissions to "Git SSH keys" in order to upload the public key. Additionally, a Coder admin must also have the `admin:public_key` scope added to the external auth configuration of the Coder deployment. For example:
|
||||
|
||||
```
|
||||
CODER_EXTERNAL_AUTH_0_ID="USER_DEFINED_ID"
|
||||
CODER_EXTERNAL_AUTH_0_TYPE=github
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key"
|
||||
```
|
||||
|
||||
Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes.
|
||||
|
||||
# Example
|
||||
|
||||
Using a coder github external auth with a non-default id: (default is `github`)
|
||||
|
||||
```tf
|
||||
data "coder_external_auth" "github" {
|
||||
id = "myauthid"
|
||||
}
|
||||
|
||||
module "github-upload-public-key" {
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
external_auth_id = data.coder_external_auth.github.id
|
||||
}
|
||||
```
|
||||
128
github-upload-public-key/main.test.ts
Normal file
128
github-upload-public-key/main.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
writeCoder,
|
||||
} from "../test";
|
||||
import { Server, serve } from "bun";
|
||||
|
||||
describe("github-upload-public-key", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
let exec = await execContainer(id, [
|
||||
"env",
|
||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
// we need to increase timeout to pull the container
|
||||
}, 15000);
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
let exec = await execContainer(id, [
|
||||
"env",
|
||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === "/user/keys") {
|
||||
if (req.method === "POST") {
|
||||
return createJSONResponse(
|
||||
{
|
||||
key: "created",
|
||||
},
|
||||
201,
|
||||
);
|
||||
}
|
||||
|
||||
// case: key already exists
|
||||
if (req.headers.get("Authorization") == "Bearer findkey") {
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
key: "exists",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// case: key does not exist
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return createJSONResponse(
|
||||
{
|
||||
error: "not_found",
|
||||
},
|
||||
404,
|
||||
);
|
||||
},
|
||||
port: 0,
|
||||
});
|
||||
|
||||
return fakeSlackHost;
|
||||
};
|
||||
43
github-upload-public-key/main.tf
Normal file
43
github-upload-public-key/main.tf
Normal file
@@ -0,0 +1,43 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "The ID of the GitHub external auth."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_api_url" {
|
||||
type = string
|
||||
description = "The URL of the GitHub instance."
|
||||
default = "https://api.github.com"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_script" "github_upload_public_key" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_OWNER_SESSION_TOKEN : data.coder_workspace_owner.me.session_token,
|
||||
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
|
||||
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
|
||||
GITHUB_API_URL : var.github_api_url,
|
||||
})
|
||||
display_name = "Github Upload Public Key"
|
||||
icon = "/icon/github.svg"
|
||||
run_on_start = true
|
||||
}
|
||||
110
github-upload-public-key/run.sh
Executable file
110
github-upload-public-key/run.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$CODER_ACCESS_URL" ]; then
|
||||
if [ -z "${CODER_ACCESS_URL}" ]; then
|
||||
echo "CODER_ACCESS_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_ACCESS_URL=${CODER_ACCESS_URL}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_OWNER_SESSION_TOKEN" ]; then
|
||||
if [ -z "${CODER_OWNER_SESSION_TOKEN}" ]; then
|
||||
echo "CODER_OWNER_SESSION_TOKEN is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_OWNER_SESSION_TOKEN=${CODER_OWNER_SESSION_TOKEN}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_EXTERNAL_AUTH_ID" ]; then
|
||||
if [ -z "${CODER_EXTERNAL_AUTH_ID}" ]; then
|
||||
echo "CODER_EXTERNAL_AUTH_ID is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_EXTERNAL_AUTH_ID=${CODER_EXTERNAL_AUTH_ID}
|
||||
fi
|
||||
|
||||
if [ -z "$GITHUB_API_URL" ]; then
|
||||
if [ -z "${GITHUB_API_URL}" ]; then
|
||||
echo "GITHUB_API_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
GITHUB_API_URL=${GITHUB_API_URL}
|
||||
fi
|
||||
|
||||
echo "Fetching GitHub token..."
|
||||
GITHUB_TOKEN=$(coder external-auth access-token $CODER_EXTERNAL_AUTH_ID)
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "Authenticate with Github to automatically upload Coder public key:\n$GITHUB_TOKEN\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public key from Coder..."
|
||||
PUBLIC_KEY_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H 'accept: application/json' \
|
||||
-H "cookie: coder_session_token=$CODER_OWNER_SESSION_TOKEN" \
|
||||
"$CODER_ACCESS_URL/api/v2/users/me/gitsshkey"
|
||||
)
|
||||
PUBLIC_KEY_RESPONSE_STATUS=$(tail -n1 <<< "$PUBLIC_KEY_RESPONSE")
|
||||
PUBLIC_KEY_BODY=$(sed \$d <<< "$PUBLIC_KEY_RESPONSE")
|
||||
|
||||
if [ "$PUBLIC_KEY_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $PUBLIC_KEY_RESPONSE_STATUS!"
|
||||
echo "$PUBLIC_KEY_BODY"
|
||||
exit 1
|
||||
fi
|
||||
PUBLIC_KEY=$(jq -r '.public_key' <<< "$PUBLIC_KEY_BODY")
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
echo "No Coder public SSH key found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public keys from GitHub..."
|
||||
GITHUB_KEYS_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys
|
||||
)
|
||||
GITHUB_KEYS_RESPONSE_STATUS=$(tail -n1 <<< "$GITHUB_KEYS_RESPONSE")
|
||||
GITHUB_KEYS_RESPONSE_BODY=$(sed \$d <<< "$GITHUB_KEYS_RESPONSE")
|
||||
|
||||
if [ "$GITHUB_KEYS_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $GITHUB_KEYS_RESPONSE_STATUS!"
|
||||
echo "$GITHUB_KEYS_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GITHUB_MATCH=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$GITHUB_KEYS_RESPONSE_BODY")
|
||||
|
||||
if [ "$PUBLIC_KEY" = "$GITHUB_MATCH" ]; then
|
||||
echo "Your Coder public key is already on GitHub!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Your Coder public key is not in GitHub. Adding it now..."
|
||||
CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces"
|
||||
UPLOAD_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-X POST \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys \
|
||||
-d "{\"title\":\"$CODER_PUBLIC_KEY_NAME\",\"key\":\"$PUBLIC_KEY\"}"
|
||||
)
|
||||
UPLOAD_RESPONSE_STATUS=$(tail -n1 <<< "$UPLOAD_RESPONSE")
|
||||
UPLOAD_RESPONSE_BODY=$(sed \$d <<< "$UPLOAD_RESPONSE")
|
||||
|
||||
if [ "$UPLOAD_RESPONSE_STATUS" -ne 201 ]; then
|
||||
echo "Failed to upload Coder public SSH key with status code $UPLOAD_RESPONSE_STATUS!"
|
||||
echo "$UPLOAD_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Your Coder public key has been added to GitHub!"
|
||||
@@ -14,7 +14,7 @@ This module adds a JetBrains Gateway Button to open any workspace with a single
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.9"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "example"
|
||||
folder = "/home/coder/example"
|
||||
@@ -27,12 +27,12 @@ module "jetbrains_gateway" {
|
||||
|
||||
## Examples
|
||||
|
||||
### Add GoLand and WebStorm with the default set to GoLand
|
||||
### Add GoLand and WebStorm as options with the default set to GoLand
|
||||
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.9"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "example"
|
||||
folder = "/home/coder/example"
|
||||
@@ -41,6 +41,37 @@ module "jetbrains_gateway" {
|
||||
}
|
||||
```
|
||||
|
||||
### Use the latest release version
|
||||
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "example"
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
default = "GO"
|
||||
latest = true
|
||||
}
|
||||
```
|
||||
|
||||
### Use the latest EAP version
|
||||
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "example"
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
default = "GO"
|
||||
latest = true
|
||||
channel = "eap"
|
||||
}
|
||||
```
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
This module and JetBrains Gateway support the following JetBrains IDEs:
|
||||
|
||||
@@ -6,6 +6,10 @@ terraform {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
version = ">= 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +50,22 @@ variable "coder_parameter_order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "latest" {
|
||||
type = bool
|
||||
description = "Whether to fetch the latest version of the IDE."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "channel" {
|
||||
type = string
|
||||
description = "JetBrains IDE release channel. Valid values are release and eap."
|
||||
default = "release"
|
||||
validation {
|
||||
condition = can(regex("^(release|eap)$", var.channel))
|
||||
error_message = "The channel must be either release or eap."
|
||||
}
|
||||
}
|
||||
|
||||
variable "jetbrains_ide_versions" {
|
||||
type = map(object({
|
||||
build_number = string
|
||||
@@ -54,36 +74,36 @@ variable "jetbrains_ide_versions" {
|
||||
description = "The set of versions for each jetbrains IDE"
|
||||
default = {
|
||||
"IU" = {
|
||||
build_number = "233.14808.21"
|
||||
version = "2023.3.5"
|
||||
build_number = "241.14494.240"
|
||||
version = "2024.1"
|
||||
}
|
||||
"PS" = {
|
||||
build_number = "233.14808.18"
|
||||
version = "2023.3.5"
|
||||
build_number = "241.14494.237"
|
||||
version = "2024.1"
|
||||
}
|
||||
"WS" = {
|
||||
build_number = "233.14475.40"
|
||||
version = "2023.3.4"
|
||||
build_number = "241.14494.235"
|
||||
version = "2024.1"
|
||||
}
|
||||
"PY" = {
|
||||
build_number = "233.14475.56"
|
||||
version = "2023.3.4"
|
||||
build_number = "241.14494.241"
|
||||
version = "2024.1"
|
||||
}
|
||||
"CL" = {
|
||||
build_number = "233.14475.31"
|
||||
version = "2023.3.4"
|
||||
build_number = "241.14494.288"
|
||||
version = "2024.1"
|
||||
}
|
||||
"GO" = {
|
||||
build_number = "233.14808.20"
|
||||
version = "2023.3.5"
|
||||
build_number = "241.14494.238"
|
||||
version = "2024.1"
|
||||
}
|
||||
"RM" = {
|
||||
build_number = "233.14808.14"
|
||||
version = "2023.3.5"
|
||||
build_number = "241.14494.234"
|
||||
version = "2024.1"
|
||||
}
|
||||
"RD" = {
|
||||
build_number = "233.14475.66"
|
||||
version = "2023.3.4"
|
||||
build_number = "241.14494.307"
|
||||
version = "2024.1"
|
||||
}
|
||||
}
|
||||
validation {
|
||||
@@ -120,6 +140,11 @@ variable "jetbrains_ides" {
|
||||
}
|
||||
}
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = var.latest ? toset(var.jetbrains_ides) : toset([])
|
||||
url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}"
|
||||
}
|
||||
|
||||
locals {
|
||||
jetbrains_ides = {
|
||||
"GO" = {
|
||||
@@ -128,6 +153,7 @@ locals {
|
||||
identifier = "GO",
|
||||
build_number = var.jetbrains_ide_versions["GO"].build_number,
|
||||
download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["GO"].version
|
||||
},
|
||||
"WS" = {
|
||||
icon = "/icon/webstorm.svg",
|
||||
@@ -135,6 +161,7 @@ locals {
|
||||
identifier = "WS",
|
||||
build_number = var.jetbrains_ide_versions["WS"].build_number,
|
||||
download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["WS"].version
|
||||
},
|
||||
"IU" = {
|
||||
icon = "/icon/intellij.svg",
|
||||
@@ -142,6 +169,7 @@ locals {
|
||||
identifier = "IU",
|
||||
build_number = var.jetbrains_ide_versions["IU"].build_number,
|
||||
download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["IU"].version
|
||||
},
|
||||
"PY" = {
|
||||
icon = "/icon/pycharm.svg",
|
||||
@@ -149,6 +177,7 @@ locals {
|
||||
identifier = "PY",
|
||||
build_number = var.jetbrains_ide_versions["PY"].build_number,
|
||||
download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PY"].version
|
||||
},
|
||||
"CL" = {
|
||||
icon = "/icon/clion.svg",
|
||||
@@ -156,6 +185,7 @@ locals {
|
||||
identifier = "CL",
|
||||
build_number = var.jetbrains_ide_versions["CL"].build_number,
|
||||
download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["CL"].version
|
||||
},
|
||||
"PS" = {
|
||||
icon = "/icon/phpstorm.svg",
|
||||
@@ -163,6 +193,7 @@ locals {
|
||||
identifier = "PS",
|
||||
build_number = var.jetbrains_ide_versions["PS"].build_number,
|
||||
download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PS"].version
|
||||
},
|
||||
"RM" = {
|
||||
icon = "/icon/rubymine.svg",
|
||||
@@ -170,6 +201,7 @@ locals {
|
||||
identifier = "RM",
|
||||
build_number = var.jetbrains_ide_versions["RM"].build_number,
|
||||
download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RM"].version
|
||||
}
|
||||
"RD" = {
|
||||
icon = "/icon/rider.svg",
|
||||
@@ -177,8 +209,18 @@ locals {
|
||||
identifier = "RD",
|
||||
build_number = var.jetbrains_ide_versions["RD"].build_number,
|
||||
download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RD"].version
|
||||
}
|
||||
}
|
||||
|
||||
icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon
|
||||
json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {}
|
||||
key = var.latest ? keys(local.json_data)[0] : ""
|
||||
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
|
||||
identifier = data.coder_parameter.jetbrains_ide.value
|
||||
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number
|
||||
version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
|
||||
}
|
||||
|
||||
data "coder_parameter" "jetbrains_ide" {
|
||||
@@ -193,9 +235,9 @@ data "coder_parameter" "jetbrains_ide" {
|
||||
dynamic "option" {
|
||||
for_each = var.jetbrains_ides
|
||||
content {
|
||||
icon = lookup(local.jetbrains_ides, option.value).icon
|
||||
name = lookup(local.jetbrains_ides, option.value).name
|
||||
value = lookup(local.jetbrains_ides, option.value).identifier
|
||||
icon = local.jetbrains_ides[option.value].icon
|
||||
name = local.jetbrains_ides[option.value].name
|
||||
value = option.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,8 +247,8 @@ data "coder_workspace" "me" {}
|
||||
resource "coder_app" "gateway" {
|
||||
agent_id = var.agent_id
|
||||
slug = "gateway"
|
||||
display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE")
|
||||
icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg")
|
||||
display_name = local.display_name
|
||||
icon = local.icon
|
||||
external = true
|
||||
order = var.order
|
||||
url = join("", [
|
||||
@@ -221,36 +263,36 @@ resource "coder_app" "gateway" {
|
||||
"&token=",
|
||||
"$SESSION_TOKEN",
|
||||
"&ide_product_code=",
|
||||
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier,
|
||||
data.coder_parameter.jetbrains_ide.value,
|
||||
"&ide_build_number=",
|
||||
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number,
|
||||
local.build_number,
|
||||
"&ide_download_link=",
|
||||
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
local.download_link,
|
||||
])
|
||||
}
|
||||
|
||||
output "identifier" {
|
||||
value = data.coder_parameter.jetbrains_ide.value
|
||||
value = local.identifier
|
||||
}
|
||||
|
||||
output "name" {
|
||||
value = coder_app.gateway.display_name
|
||||
output "display_name" {
|
||||
value = local.display_name
|
||||
}
|
||||
|
||||
output "icon" {
|
||||
value = coder_app.gateway.icon
|
||||
value = local.icon
|
||||
}
|
||||
|
||||
output "download_link" {
|
||||
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link
|
||||
value = local.download_link
|
||||
}
|
||||
|
||||
output "build_number" {
|
||||
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number
|
||||
value = local.build_number
|
||||
}
|
||||
|
||||
output "version" {
|
||||
value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
|
||||
value = local.version
|
||||
}
|
||||
|
||||
output "url" {
|
||||
|
||||
@@ -17,7 +17,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -44,7 +44,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
@@ -72,7 +72,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12.4"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,12 @@ EOF
|
||||
|
||||
locals {
|
||||
# The username field to use for artifactory
|
||||
username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
|
||||
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
||||
jfrog_host = replace(var.jfrog_url, "https://", "")
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_external_auth" "jfrog" {
|
||||
id = var.external_auth_id
|
||||
@@ -87,7 +88,7 @@ resource "coder_script" "jfrog" {
|
||||
JFROG_HOST : local.jfrog_host,
|
||||
JFROG_SERVER_ID : var.jfrog_server_id,
|
||||
ARTIFACTORY_USERNAME : local.username,
|
||||
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
|
||||
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
|
||||
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,
|
||||
CONFIGURE_CODE_SERVER : var.configure_code_server,
|
||||
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
|
||||
|
||||
@@ -15,7 +15,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
@@ -41,7 +41,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://YYYY.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token # An admin access token
|
||||
@@ -74,7 +74,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
@@ -94,11 +94,11 @@ data "coder_workspace" "me" {}
|
||||
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
artifactory_access_token = var.artifactory_access_token
|
||||
token_description = "Token for Coder workspace: ${data.coder_workspace.me.owner}/${data.coder_workspace.me.name}"
|
||||
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}"
|
||||
package_managers = {
|
||||
"npm" : "npm",
|
||||
"go" : "go",
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12.4"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
artifactory = {
|
||||
source = "registry.terraform.io/jfrog/artifactory"
|
||||
@@ -95,7 +95,7 @@ EOF
|
||||
|
||||
locals {
|
||||
# The username field to use for artifactory
|
||||
username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
|
||||
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
||||
jfrog_host = replace(var.jfrog_url, "https://", "")
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ resource "artifactory_scoped_token" "me" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_script" "jfrog" {
|
||||
agent_id = var.agent_id
|
||||
@@ -127,7 +128,7 @@ resource "coder_script" "jfrog" {
|
||||
JFROG_HOST : local.jfrog_host,
|
||||
JFROG_SERVER_ID : var.jfrog_server_id,
|
||||
ARTIFACTORY_USERNAME : local.username,
|
||||
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
|
||||
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
|
||||
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
|
||||
CONFIGURE_CODE_SERVER : var.configure_code_server,
|
||||
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
|
||||
|
||||
15
lint.ts
15
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);
|
||||
}
|
||||
|
||||
263
package-lock.json
generated
263
package-lock.json
generated
@@ -1,263 +0,0 @@
|
||||
{
|
||||
"name": "modules",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "modules",
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.0.18",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^12.0.0",
|
||||
"prettier-plugin-sh": "^0.13.1",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
|
||||
"integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz",
|
||||
"integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "~20.12.8",
|
||||
"@types/ws": "~8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extendable": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1",
|
||||
"kind-of": "^6.0.2",
|
||||
"section-matter": "^1.0.0",
|
||||
"strip-bom-string": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extendable": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mvdan-sh": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
|
||||
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-sh": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
|
||||
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mvdan-sh": "^0.10.1",
|
||||
"sh-syntax": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-terraform-formatter": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
|
||||
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"prettier": ">= 1.16.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/section-matter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"kind-of": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/sh-syntax": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
|
||||
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/strip-bom-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +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",
|
||||
"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": [
|
||||
@@ -23,4 +24,4 @@
|
||||
"prettier-plugin-terraform-formatter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
writeCoder,
|
||||
} from "../test";
|
||||
|
||||
describe("slackme", async () => {
|
||||
@@ -119,22 +120,16 @@ const setupContainer = async (
|
||||
return { id, instance };
|
||||
};
|
||||
|
||||
const writeCoder = async (id: string, script: string) => {
|
||||
const exec = await execContainer(id, [
|
||||
"sh",
|
||||
"-c",
|
||||
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
|
||||
]);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const assertSlackMessage = async (opts: {
|
||||
command: string;
|
||||
format?: string;
|
||||
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);
|
||||
@@ -146,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",
|
||||
@@ -162,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");
|
||||
|
||||
60
test.ts
60
test.ts
@@ -78,21 +78,33 @@ export const execContainer = async (
|
||||
};
|
||||
};
|
||||
|
||||
type JsonValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
type TerraformStateResource = {
|
||||
type: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
instances: [{ attributes: Record<string, any> }];
|
||||
|
||||
instances: [
|
||||
{
|
||||
attributes: Record<string, JsonValue>;
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type TerraformOutput = {
|
||||
type: string;
|
||||
value: JsonValue;
|
||||
};
|
||||
|
||||
export interface TerraformState {
|
||||
outputs: {
|
||||
[key: string]: {
|
||||
type: string;
|
||||
value: any;
|
||||
};
|
||||
};
|
||||
|
||||
outputs: Record<string, TerraformOutput>;
|
||||
resources: [TerraformStateResource, ...TerraformStateResource[]];
|
||||
}
|
||||
|
||||
@@ -141,19 +153,25 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
|
||||
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<string, string> = {};
|
||||
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`,
|
||||
);
|
||||
@@ -170,14 +188,19 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
|
||||
* random state file.
|
||||
*/
|
||||
export const runTerraformApply = async <
|
||||
TVars extends Readonly<Record<string, string>>,
|
||||
TVars extends Readonly<Record<string, string | boolean>>,
|
||||
>(
|
||||
dir: string,
|
||||
vars: TVars,
|
||||
env?: Record<string, string>,
|
||||
): Promise<TerraformState> => {
|
||||
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
|
||||
const env = {};
|
||||
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));
|
||||
|
||||
const combinedEnv = env === undefined ? {} : { ...env };
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
combinedEnv[`TF_VAR_${key}`] = String(value);
|
||||
}
|
||||
|
||||
const proc = spawn(
|
||||
[
|
||||
"terraform",
|
||||
@@ -191,16 +214,18 @@ export const runTerraformApply = async <
|
||||
],
|
||||
{
|
||||
cwd: dir,
|
||||
env,
|
||||
env: combinedEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
},
|
||||
);
|
||||
|
||||
const text = await readableStreamToText(proc.stderr);
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const content = await readFile(stateFile, "utf8");
|
||||
await unlink(stateFile);
|
||||
return JSON.parse(content);
|
||||
@@ -228,3 +253,12 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
|
||||
status: statusCode,
|
||||
});
|
||||
};
|
||||
|
||||
export const writeCoder = async (id: string, script: string) => {
|
||||
const exec = await execContainer(id, [
|
||||
"sh",
|
||||
"-c",
|
||||
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
|
||||
]);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"types": ["bun-types"]
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
```tf
|
||||
module "vscode" {
|
||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -28,7 +28,7 @@ module "vscode" {
|
||||
```tf
|
||||
module "vscode" {
|
||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -18,11 +18,58 @@ describe("vscode-desktop", async () => {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.vscode_url.value).toBe(
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const resources = state.resources;
|
||||
expect(resources[1].instances[0].attributes.order).toBeNull();
|
||||
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();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.vscode_url.value).toBe(
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.vscode_url.value).toBe(
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
});
|
||||
expect(state.outputs.vscode_url.value).toBe(
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.vscode_url.value).toBe(
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
@@ -31,7 +78,12 @@ describe("vscode-desktop", async () => {
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const resources = state.resources;
|
||||
expect(resources[1].instances[0].attributes.order).toBe(22);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ variable "folder" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
@@ -27,6 +33,7 @@ variable "order" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "vscode" {
|
||||
agent_id = var.agent_id
|
||||
@@ -35,22 +42,17 @@ resource "coder_app" "vscode" {
|
||||
slug = "vscode"
|
||||
display_name = "VS Code Desktop"
|
||||
order = var.order
|
||||
url = var.folder != "" ? join("", [
|
||||
"vscode://coder.coder-remote/open?owner=",
|
||||
data.coder_workspace.me.owner,
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
"&folder=",
|
||||
var.folder,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
]) : join("", [
|
||||
"vscode://coder.coder-remote/open?owner=",
|
||||
data.coder_workspace.me.owner,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.14"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@@ -29,7 +29,7 @@ module "vscode-web" {
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.14"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@@ -42,7 +42,7 @@ module "vscode-web" {
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.14"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
@@ -56,7 +56,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.10"
|
||||
version = "1.0.14"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
|
||||
42
vscode-web/main.test.ts
Normal file
42
vscode-web/main.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { runTerraformApply, runTerraformInit } from "../test";
|
||||
|
||||
describe("vscode-web", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
it("accept_license should be set to true", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "false",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Invalid value for variable");
|
||||
});
|
||||
|
||||
it("use_cached and offline can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
use_cached: "true",
|
||||
offline: "true",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline and Use Cached can not be used together");
|
||||
});
|
||||
|
||||
it("offline and extensions can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
offline: "true",
|
||||
extensions: '["1", "2"]',
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline mode does not allow extensions to be installed");
|
||||
});
|
||||
|
||||
// More tests depend on shebang refactors
|
||||
});
|
||||
@@ -97,6 +97,30 @@ variable "settings" {
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "offline" {
|
||||
type = bool
|
||||
description = "Just run VS Code Web in the background, don't fetch it from the internet."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "use_cached" {
|
||||
type = bool
|
||||
description = "Uses cached copy of VS Code Web in the background, otherwise fetches it from internet."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extensions_dir" {
|
||||
type = string
|
||||
description = "Override the directory to store extensions in."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "auto_install_extensions" {
|
||||
type = bool
|
||||
description = "Automatically install recommended extensions when VS Code Web starts."
|
||||
default = false
|
||||
}
|
||||
|
||||
resource "coder_script" "vscode-web" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "VS Code Web"
|
||||
@@ -109,8 +133,25 @@ resource "coder_script" "vscode-web" {
|
||||
TELEMETRY_LEVEL : var.telemetry_level,
|
||||
// This is necessary otherwise the quotes are stripped!
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
EXTENSIONS_DIR : var.extensions_dir,
|
||||
FOLDER : var.folder,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = !var.offline || length(var.extensions) == 0
|
||||
error_message = "Offline mode does not allow extensions to be installed"
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = !var.offline || !var.use_cached
|
||||
error_message = "Offline and Use Cached can not be used together"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "vscode-web" {
|
||||
|
||||
@@ -2,6 +2,40 @@
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
EXTENSIONS=("${EXTENSIONS}")
|
||||
VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server"
|
||||
|
||||
# Set extension directory
|
||||
EXTENSION_ARG=""
|
||||
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||
fi
|
||||
|
||||
run_vscode_web() {
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
|
||||
echo "⚙️ Creating settings file..."
|
||||
mkdir -p ~/.vscode-server/data/Machine
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
|
||||
fi
|
||||
|
||||
# Check if vscode-server is already installed for offline or cached mode
|
||||
if [ -f "$VSCODE_WEB" ]; then
|
||||
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
||||
echo "🥳 Found a copy of VS Code Web"
|
||||
run_vscode_web
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# Offline mode always expects a copy of vscode-server to be present
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
echo "Failed to find a copy of VS Code Web"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install prefix
|
||||
mkdir -p ${INSTALL_PREFIX}
|
||||
@@ -26,9 +60,7 @@ if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Microsoft Visual Studio Code Server: $output"
|
||||
exit 1
|
||||
fi
|
||||
printf "$${BOLD}Microsoft Visual Studio Code Server has been installed.\n"
|
||||
|
||||
VSCODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
|
||||
printf "$${BOLD}VS Code Web has been installed.\n"
|
||||
|
||||
# Install each extension...
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
@@ -37,20 +69,31 @@ for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
continue
|
||||
fi
|
||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||
output=$($VSCODE_SERVER --install-extension "$extension" --force)
|
||||
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install extension: $extension: $output"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the settings file exists...
|
||||
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
|
||||
echo "⚙️ Creating settings file..."
|
||||
mkdir -p ~/.vscode-server/data/Machine
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
fi
|
||||
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
|
||||
for extension in $extensions; do
|
||||
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"${INSTALL_PREFIX}/bin/code-server" serve-local --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
run_vscode_web
|
||||
|
||||
@@ -1,35 +1,57 @@
|
||||
---
|
||||
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).
|
||||
|
||||
[](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538)
|
||||
```tf
|
||||
# AWS example. See below for examples of using this module with other providers
|
||||
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
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Video
|
||||
|
||||
[](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02)
|
||||
|
||||
## Examples
|
||||
|
||||
### With AWS
|
||||
|
||||
```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
|
||||
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
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
### With Google Cloud
|
||||
|
||||
```tf
|
||||
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
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
}
|
||||
```
|
||||
|
||||
## Tested on
|
||||
|
||||
- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Test on additional cloud providers
|
||||
- [ ] Automatically establish web RDP session when users click "web RDP"
|
||||
> This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality.
|
||||
- [ ] Test on Microsoft Azure.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
executeScriptInContainer,
|
||||
TerraformState,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
@@ -9,10 +9,38 @@ import {
|
||||
type TestVariables = Readonly<{
|
||||
agent_id: string;
|
||||
resource_id: string;
|
||||
share?: string;
|
||||
admin_username?: string;
|
||||
admin_password?: string;
|
||||
}>;
|
||||
|
||||
function findWindowsRdpScript(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" &&
|
||||
typeof instance.attributes.script === "string"
|
||||
) {
|
||||
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,
|
||||
* but the current test setup doesn't really make that viable
|
||||
*/
|
||||
describe("Web RDP", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
@@ -20,30 +48,48 @@ 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<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
throw new Error("Not implemented yet");
|
||||
});
|
||||
const lines = findWindowsRdpScript(state)
|
||||
?.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => line.trim());
|
||||
|
||||
/**
|
||||
* @todo Verify that the HTML file has been modified, and that the JS file is
|
||||
* also part of the file system
|
||||
*/
|
||||
it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
});
|
||||
|
||||
throw new Error("Not implemented yet");
|
||||
expect(lines).toEqual(
|
||||
expect.arrayContaining<string>([
|
||||
'$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 () => {
|
||||
throw new Error("Not implemented yet");
|
||||
/**
|
||||
* 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 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. 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}
|
||||
*/
|
||||
const formEntryValuesRe =
|
||||
/^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?<username>.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?<password>.+?)",$.*?^};$/ms;
|
||||
|
||||
// Test that things work with the default username/password
|
||||
const defaultState = await runTerraformApply<TestVariables>(
|
||||
@@ -54,19 +100,35 @@ describe("Web RDP", async () => {
|
||||
},
|
||||
);
|
||||
|
||||
const output = await executeScriptInContainer(defaultState, "alpine");
|
||||
const defaultRdpScript = findWindowsRdpScript(defaultState);
|
||||
expect(defaultRdpScript).toBeString();
|
||||
|
||||
const defaultResultsGroup =
|
||||
formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {};
|
||||
|
||||
expect(defaultResultsGroup.username).toBe("Administrator");
|
||||
expect(defaultResultsGroup.password).toBe("coderRDP!");
|
||||
|
||||
// Test that custom usernames/passwords are also forwarded correctly
|
||||
const customUsername = "crouton";
|
||||
const customPassword = "VeryVeryVeryVeryVerySecurePassword97!";
|
||||
const customAdminUsername = "crouton";
|
||||
const customAdminPassword = "VeryVeryVeryVeryVerySecurePassword97!";
|
||||
const customizedState = await runTerraformApply<TestVariables>(
|
||||
import.meta.dir,
|
||||
{
|
||||
agent_id: "foo",
|
||||
resource_id: "bar",
|
||||
admin_username: customUsername,
|
||||
admin_password: customPassword,
|
||||
admin_username: customAdminUsername,
|
||||
admin_password: customAdminPassword,
|
||||
},
|
||||
);
|
||||
|
||||
const customRdpScript = findWindowsRdpScript(customizedState);
|
||||
expect(customRdpScript).toBeString();
|
||||
|
||||
const customResultsGroup =
|
||||
formEntryValuesRe.exec(customRdpScript ?? "")?.groups ?? {};
|
||||
|
||||
expect(customResultsGroup.username).toBe(customAdminUsername);
|
||||
expect(customResultsGroup.password).toBe(customAdminPassword);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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."
|
||||
@@ -33,98 +42,31 @@ 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
|
||||
script = <<EOF
|
||||
function Set-AdminPassword {
|
||||
param (
|
||||
[string]$adminPassword
|
||||
)
|
||||
# Set admin password
|
||||
Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
|
||||
# Enable admin user
|
||||
Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser
|
||||
}
|
||||
icon = "/icon/desktop.svg"
|
||||
|
||||
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"
|
||||
}
|
||||
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
|
||||
admin_username = var.admin_username
|
||||
admin_password = var.admin_password
|
||||
|
||||
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
|
||||
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 = '<script defer id="coder-patch" src="coder.js"></script>'
|
||||
|
||||
# Always copy the file in case we change it.
|
||||
@'
|
||||
${templatefile("${path.module}/devolutions-patch.js", {
|
||||
CODER_USERNAME : var.admin_username,
|
||||
CODER_PASSWORD : var.admin_password,
|
||||
})}
|
||||
'@ | Set-Content "$root\coder.js"
|
||||
|
||||
# Only inject the src if we have not before.
|
||||
$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch
|
||||
if ($isPatched -eq $null) {
|
||||
(Get-Content $devolutionsHtml).Replace('</app-root>', "</app-root>$patch") | Set-Content $devolutionsHtml
|
||||
}
|
||||
}
|
||||
|
||||
Set-AdminPassword -adminPassword "${var.admin_password}"
|
||||
Configure-RDP
|
||||
Install-DevolutionsGateway
|
||||
Patch-Devolutions-HTML
|
||||
|
||||
EOF
|
||||
# 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
|
||||
})
|
||||
})
|
||||
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "windows-rdp" {
|
||||
agent_id = var.agent_id
|
||||
share = var.share
|
||||
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 {
|
||||
@@ -139,29 +81,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
|
||||
}
|
||||
|
||||
# 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
|
||||
# }
|
||||
# }
|
||||
|
||||
85
windows-rdp/powershell-installation-script.tftpl
Normal file
85
windows-rdp/powershell-installation-script.tftpl
Normal file
@@ -0,0 +1,85 @@
|
||||
function Set-AdminPassword {
|
||||
param (
|
||||
[string]$adminPassword
|
||||
)
|
||||
# Set admin password
|
||||
Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
|
||||
# Enable admin user
|
||||
Get-LocalUser -Name "${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 = '<script defer id="coder-patch" src="coder.js"></script>'
|
||||
|
||||
# Always copy the file in case we change it.
|
||||
@'
|
||||
${patch_file_contents}
|
||||
'@ | Set-Content "$root\coder.js"
|
||||
|
||||
# Only inject the src if we have not before.
|
||||
$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch
|
||||
if ($isPatched -eq $null) {
|
||||
(Get-Content $devolutionsHtml).Replace('</app-root>', "</app-root>$patch") | Set-Content $devolutionsHtml
|
||||
}
|
||||
}
|
||||
|
||||
Set-AdminPassword -adminPassword "${admin_password}"
|
||||
Configure-RDP
|
||||
Install-DevolutionsGateway
|
||||
Patch-Devolutions-HTML
|
||||
BIN
windows-rdp/video-thumbnails/video-thumbnail.png
Normal file
BIN
windows-rdp/video-thumbnails/video-thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
Reference in New Issue
Block a user