From cb72a16221728ba8eaa01bb02cb9a93e72b3354a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 25 Sep 2023 23:17:04 -0500 Subject: [PATCH] feat: add test framework (#48) * Add test framework * Add aws-region tests * Add azure-region tests * Update CONTRIBUTING.md * Add formatting * Improve fmt * Format Terraform --- .github/workflows/ci.yaml | 31 ++ .gitignore | 5 +- .vscode/settings.json | 6 + CONTRIBUTING.md | 34 +-- aws-region/main.test.ts | 25 ++ aws-region/main.tf | 202 ++++++------- azure-region/main.test.ts | 25 ++ azure-region/main.tf | 564 ++++++++++++++++++------------------ bun.lockb | Bin 0 -> 1269 bytes bunfig.toml | 2 + code-server/main.tf | 64 ++-- fly-region/README.md | 2 +- gcp-region/README.md | 47 +-- git-clone/README.md | 1 + git-clone/main.test.ts | 39 +++ git-clone/main.tf | 24 +- jetbrains-gateway/README.md | 1 + jfrog/README.md | 2 +- package.json | 14 + personalize/main.tf | 30 +- setup.ts | 47 +++ test.ts | 212 ++++++++++++++ tsconfig.json | 7 + vscode-desktop/main.tf | 26 +- 24 files changed, 900 insertions(+), 510 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .vscode/settings.json create mode 100644 aws-region/main.test.ts create mode 100644 azure-region/main.test.ts create mode 100755 bun.lockb create mode 100644 bunfig.toml create mode 100644 git-clone/main.test.ts create mode 100644 package.json create mode 100644 setup.ts create mode 100644 test.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8117c47 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: ci + +on: + push: + branches: + - main + + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - run: bun test + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - run: bun fmt:ci \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66df410..6d6f5a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.terraform* \ No newline at end of file +.terraform* +node_modules +*.tfstate +*.tfstate.lock.info \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1c5485b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.exclude": { + "**/terraform.tfstate": true, + "**/.terraform": true + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb8d484..8311b04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,37 +6,13 @@ To create a new module, clone this repository and run: ./new.sh MOUDLE_NAME ``` -Test a module by running an instance of Coder on your local machine: - -```shell -coder server --in-memory -``` - -This will create a new module in the modules directory with the given name and scaffolding. -Edit the files, adding your module's implementation, documentation and screenshots. - ## Testing a Module -Create a template and edit it to include your development module: +A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. -> [!NOTE] -> The Docker starter template is recommended for quick-iteration! +Reference existing `*.test.ts` files for implementation. -```hcl -module "MOUDLE_NAME" { - source = "/home/user/coder/modules/MOUDLE_NAME" -} +```sh +# Run tests for a specific module! +$ bun test -t '' ``` - -You can also test your module by specifying the source as a git repository: - -```hcl -module "MOUDLE_NAME" { - source = "git::https://github.com//.git//?ref=" -} -``` - -Build a workspace and your module will be consumed! 🥳 - -Open a pull-request with your module, a member of the Coder team will -manually test it, and after-merge it will appear on the Registry. diff --git a/aws-region/main.test.ts b/aws-region/main.test.ts new file mode 100644 index 0000000..f943f94 --- /dev/null +++ b/aws-region/main.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("aws-region", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, {}); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, {}); + expect(state.outputs.value.value).toBe("us-east-1"); + }); + + it("customized default", async () => { + const state = await runTerraformApply(import.meta.dir, { + default: "us-west-2", + }); + expect(state.outputs.value.value).toBe("us-west-2"); + }); +}); diff --git a/aws-region/main.tf b/aws-region/main.tf index 11f43b5..2ab5cd2 100644 --- a/aws-region/main.tf +++ b/aws-region/main.tf @@ -10,45 +10,45 @@ terraform { } variable "display_name" { - default = "AWS Region" - description = "The display name of the parameter." - type = string + default = "AWS Region" + description = "The display name of the parameter." + type = string } variable "description" { - default = "The region to deploy workspace infrastructure." - description = "The description of the parameter." - type = string + default = "The region to deploy workspace infrastructure." + description = "The description of the parameter." + type = string } variable "default" { - default = "us-east-1" - description = "The default region to use if no region is specified." - type = string + default = "us-east-1" + description = "The default region to use if no region is specified." + type = string } variable "mutable" { - default = false - description = "Whether the parameter can be changed after creation." - type = bool + default = false + description = "Whether the parameter can be changed after creation." + type = bool } variable "custom_names" { - default = {} - description = "A map of custom display names for region IDs." - type = map(string) + default = {} + description = "A map of custom display names for region IDs." + type = map(string) } variable "custom_icons" { - default = {} - description = "A map of custom icons for region IDs." - type = map(string) + default = {} + description = "A map of custom icons for region IDs." + type = map(string) } variable "exclude" { - default = [] - description = "A list of region IDs to exclude." - type = list(string) + default = [] + description = "A list of region IDs to exclude." + type = list(string) } locals { @@ -57,92 +57,92 @@ locals { # the provider, which requires a region. regions = { "ap-northeast-1" = { - name = "Asia Pacific (Tokyo)" - icon = "/emojis/1f1ef-1f1f5.png" - } - "ap-northeast-2" = { - name = "Asia Pacific (Seoul)" - icon = "/emojis/1f1f0-1f1f7.png" - } - "ap-northeast-3" = { - name = "Asia Pacific (Osaka)" - icon = "/emojis/1f1ef-1f1f5.png" - } - "ap-south-1" = { - name = "Asia Pacific (Mumbai)" - icon = "/emojis/1f1ee-1f1f3.png" - } - "ap-southeast-1" = { - name = "Asia Pacific (Singapore)" - icon = "/emojis/1f1f8-1f1ec.png" - } - "ap-southeast-2" = { - name = "Asia Pacific (Sydney)" - icon = "/emojis/1f1e6-1f1fa.png" - } - "ca-central-1" = { - name = "Canada (Central)" - icon = "/emojis/1f1e8-1f1e6.png" - } - "eu-central-1" = { - name = "EU (Frankfurt)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "eu-north-1" = { - name = "EU (Stockholm)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "eu-west-1" = { - name = "EU (Ireland)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "eu-west-2" = { - name = "EU (London)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "eu-west-3" = { - name = "EU (Paris)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "sa-east-1" = { - name = "South America (São Paulo)" - icon = "/emojis/1f1e7-1f1f7.png" - } - "us-east-1" = { - name = "US East (N. Virginia)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "us-east-2" = { - name = "US East (Ohio)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "us-west-1" = { - name = "US West (N. California)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "us-west-2" = { - name = "US West (Oregon)" - icon = "/emojis/1f1fa-1f1f8.png" - } + name = "Asia Pacific (Tokyo)" + icon = "/emojis/1f1ef-1f1f5.png" + } + "ap-northeast-2" = { + name = "Asia Pacific (Seoul)" + icon = "/emojis/1f1f0-1f1f7.png" + } + "ap-northeast-3" = { + name = "Asia Pacific (Osaka)" + icon = "/emojis/1f1ef-1f1f5.png" + } + "ap-south-1" = { + name = "Asia Pacific (Mumbai)" + icon = "/emojis/1f1ee-1f1f3.png" + } + "ap-southeast-1" = { + name = "Asia Pacific (Singapore)" + icon = "/emojis/1f1f8-1f1ec.png" + } + "ap-southeast-2" = { + name = "Asia Pacific (Sydney)" + icon = "/emojis/1f1e6-1f1fa.png" + } + "ca-central-1" = { + name = "Canada (Central)" + icon = "/emojis/1f1e8-1f1e6.png" + } + "eu-central-1" = { + name = "EU (Frankfurt)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "eu-north-1" = { + name = "EU (Stockholm)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "eu-west-1" = { + name = "EU (Ireland)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "eu-west-2" = { + name = "EU (London)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "eu-west-3" = { + name = "EU (Paris)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "sa-east-1" = { + name = "South America (São Paulo)" + icon = "/emojis/1f1e7-1f1f7.png" + } + "us-east-1" = { + name = "US East (N. Virginia)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "us-east-2" = { + name = "US East (Ohio)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "us-west-1" = { + name = "US West (N. California)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "us-west-2" = { + name = "US West (Oregon)" + icon = "/emojis/1f1fa-1f1f8.png" + } } } data "coder_parameter" "region" { - name = "aws_region" - display_name = var.display_name - description = var.description - default = var.default - mutable = var.mutable - dynamic "option" { - for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } - content { - name = try(var.custom_names[option.key], option.value.name) - icon = try(var.custom_icons[option.key], option.value.icon) - value = option.key - } + name = "aws_region" + display_name = var.display_name + description = var.description + default = var.default + mutable = var.mutable + dynamic "option" { + for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } + content { + name = try(var.custom_names[option.key], option.value.name) + icon = try(var.custom_icons[option.key], option.value.icon) + value = option.key } + } } output "value" { - value = data.coder_parameter.region.value + value = data.coder_parameter.region.value } diff --git a/azure-region/main.test.ts b/azure-region/main.test.ts new file mode 100644 index 0000000..26a522d --- /dev/null +++ b/azure-region/main.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("azure-region", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, {}); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, {}); + expect(state.outputs.value.value).toBe("eastus"); + }); + + it("customized default", async () => { + const state = await runTerraformApply(import.meta.dir, { + default: "westus", + }); + expect(state.outputs.value.value).toBe("westus"); + }); +}); diff --git a/azure-region/main.tf b/azure-region/main.tf index 6142e77..1e38091 100644 --- a/azure-region/main.tf +++ b/azure-region/main.tf @@ -10,316 +10,316 @@ terraform { } variable "display_name" { - default = "Azure Region" - description = "The display name of the Coder parameter." - type = string + default = "Azure Region" + description = "The display name of the Coder parameter." + type = string } variable "description" { - default = "The region where your workspace will live." + default = "The region where your workspace will live." description = "Description of the Coder parameter." } variable "default" { - default = "eastus" - description = "The default region to use if no region is specified." - type = string + default = "eastus" + description = "The default region to use if no region is specified." + type = string } variable "mutable" { - default = false - description = "Whether the parameter can be changed after creation." - type = bool + default = false + description = "Whether the parameter can be changed after creation." + type = bool } variable "custom_names" { - default = {} - description = "A map of custom display names for region IDs." - type = map(string) + default = {} + description = "A map of custom display names for region IDs." + type = map(string) } variable "custom_icons" { - default = {} - description = "A map of custom icons for region IDs." - type = map(string) + default = {} + description = "A map of custom icons for region IDs." + type = map(string) } variable "exclude" { - default = [] - description = "A list of region IDs to exclude." - type = list(string) + default = [] + description = "A list of region IDs to exclude." + type = list(string) } locals { - # Note: Options are limited to 64 regions, some redundant regions have been removed. - all_regions = { - "australia" = { - name = "Australia" - icon = "/emojis/1f1e6-1f1fa.png" - } - "australiacentral" = { - name = "Australia Central" - icon = "/emojis/1f1e6-1f1fa.png" - } - "australiacentral2" = { - name = "Australia Central 2" - icon = "/emojis/1f1e6-1f1fa.png" - } - "australiaeast" = { - name = "Australia (New South Wales)" - icon = "/emojis/1f1e6-1f1fa.png" - } - "australiasoutheast" = { - name = "Australia Southeast" - icon = "/emojis/1f1e6-1f1fa.png" - } - "brazil" = { - name = "Brazil" - icon = "/emojis/1f1e7-1f1f7.png" - } - "brazilsouth" = { - name = "Brazil (Sao Paulo)" - icon = "/emojis/1f1e7-1f1f7.png" - } - "brazilsoutheast" = { - name = "Brazil Southeast" - icon = "/emojis/1f1e7-1f1f7.png" - } - "brazilus" = { - name = "Brazil US" - icon = "/emojis/1f1e7-1f1f7.png" - } - "canada" = { - name = "Canada" - icon = "/emojis/1f1e8-1f1e6.png" - } - "canadacentral" = { - name = "Canada (Toronto)" - icon = "/emojis/1f1e8-1f1e6.png" - } - "canadaeast" = { - name = "Canada East" - icon = "/emojis/1f1e8-1f1e6.png" - } - "centralindia" = { - name = "India (Pune)" - icon = "/emojis/1f1ee-1f1f3.png" - } - "centralus" = { - name = "US (Iowa)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "eastasia" = { - name = "East Asia (Hong Kong)" - icon = "/emojis/1f1f0-1f1f7.png" - } - "eastus" = { - name = "US (Virginia)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "eastus2" = { - name = "US (Virginia) 2" - icon = "/emojis/1f1fa-1f1f8.png" - } - "europe" = { - name = "Europe" - icon = "/emojis/1f30d.png" - } - "france" = { - name = "France" - icon = "/emojis/1f1eb-1f1f7.png" - } - "francecentral" = { - name = "France (Paris)" - icon = "/emojis/1f1eb-1f1f7.png" - } - "francesouth" = { - name = "France South" - icon = "/emojis/1f1eb-1f1f7.png" - } - "germany" = { - name = "Germany" - icon = "/emojis/1f1e9-1f1ea.png" - } - "germanynorth" = { - name = "Germany North" - icon = "/emojis/1f1e9-1f1ea.png" - } - "germanywestcentral" = { - name = "Germany (Frankfurt)" - icon = "/emojis/1f1e9-1f1ea.png" - } - "india" = { - name = "India" - icon = "/emojis/1f1ee-1f1f3.png" - } - "japan" = { - name = "Japan" - icon = "/emojis/1f1ef-1f1f5.png" - } - "japaneast" = { - name = "Japan (Tokyo)" - icon = "/emojis/1f1ef-1f1f5.png" - } - "japanwest" = { - name = "Japan West" - icon = "/emojis/1f1ef-1f1f5.png" - } - "jioindiacentral" = { - name = "Jio India Central" - icon = "/emojis/1f1ee-1f1f3.png" - } - "jioindiawest" = { - name = "Jio India West" - icon = "/emojis/1f1ee-1f1f3.png" - } - "koreacentral" = { - name = "Korea (Seoul)" - icon = "/emojis/1f1f0-1f1f7.png" - } - "koreasouth" = { - name = "Korea South" - icon = "/emojis/1f1f0-1f1f7.png" - } - "northcentralus" = { - name = "North Central US" - icon = "/emojis/1f1fa-1f1f8.png" - } - "northeurope" = { - name = "Europe (Ireland)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "norway" = { - name = "Norway" - icon = "/emojis/1f1f3-1f1f4.png" - } - "norwayeast" = { - name = "Norway (Oslo)" - icon = "/emojis/1f1f3-1f1f4.png" - } - "norwaywest" = { - name = "Norway West" - icon = "/emojis/1f1f3-1f1f4.png" - } - "qatarcentral" = { - name = "Qatar (Doha)" - icon = "/emojis/1f1f6-1f1e6.png" - } - "singapore" = { - name = "Singapore" - icon = "/emojis/1f1f8-1f1ec.png" - } - "southafrica" = { - name = "South Africa" - icon = "/emojis/1f1ff-1f1e6.png" - } - "southafricanorth" = { - name = "South Africa (Johannesburg)" - icon = "/emojis/1f1ff-1f1e6.png" - } - "southafricawest" = { - name = "South Africa West" - icon = "/emojis/1f1ff-1f1e6.png" - } - "southcentralus" = { - name = "US (Texas)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "southeastasia" = { - name = "Southeast Asia (Singapore)" - icon = "/emojis/1f1f0-1f1f7.png" - } - "southindia" = { - name = "South India" - icon = "/emojis/1f1ee-1f1f3.png" - } - "swedencentral" = { - name = "Sweden (Gävle)" - icon = "/emojis/1f1f8-1f1ea.png" - } - "switzerland" = { - name = "Switzerland" - icon = "/emojis/1f1e8-1f1ed.png" - } - "switzerlandnorth" = { - name = "Switzerland (Zurich)" - icon = "/emojis/1f1e8-1f1ed.png" - } - "switzerlandwest" = { - name = "Switzerland West" - icon = "/emojis/1f1e8-1f1ed.png" - } - "uae" = { - name = "United Arab Emirates" - icon = "/emojis/1f1e6-1f1ea.png" - } - "uaecentral" = { - name = "UAE Central" - icon = "/emojis/1f1e6-1f1ea.png" - } - "uaenorth" = { - name = "UAE (Dubai)" - icon = "/emojis/1f1e6-1f1ea.png" - } - "uk" = { - name = "United Kingdom" - icon = "/emojis/1f1ec-1f1e7.png" - } - "uksouth" = { - name = "UK (London)" - icon = "/emojis/1f1ec-1f1e7.png" - } - "ukwest" = { - name = "UK West" - icon = "/emojis/1f1ec-1f1e7.png" - } - "unitedstates" = { - name = "United States" - icon = "/emojis/1f1fa-1f1f8.png" - } - "westcentralus" = { - name = "West Central US" - icon = "/emojis/1f1fa-1f1f8.png" - } - "westeurope" = { - name = "Europe (Netherlands)" - icon = "/emojis/1f1ea-1f1fa.png" - } - "westindia" = { - name = "West India" - icon = "/emojis/1f1ee-1f1f3.png" - } - "westus" = { - name = "West US" - icon = "/emojis/1f1fa-1f1f8.png" - } - "westus2" = { - name = "US (Washington)" - icon = "/emojis/1f1fa-1f1f8.png" - } - "westus3" = { - name = "US (Arizona)" - icon = "/emojis/1f1fa-1f1f8.png" - } + # Note: Options are limited to 64 regions, some redundant regions have been removed. + all_regions = { + "australia" = { + name = "Australia" + icon = "/emojis/1f1e6-1f1fa.png" } + "australiacentral" = { + name = "Australia Central" + icon = "/emojis/1f1e6-1f1fa.png" + } + "australiacentral2" = { + name = "Australia Central 2" + icon = "/emojis/1f1e6-1f1fa.png" + } + "australiaeast" = { + name = "Australia (New South Wales)" + icon = "/emojis/1f1e6-1f1fa.png" + } + "australiasoutheast" = { + name = "Australia Southeast" + icon = "/emojis/1f1e6-1f1fa.png" + } + "brazil" = { + name = "Brazil" + icon = "/emojis/1f1e7-1f1f7.png" + } + "brazilsouth" = { + name = "Brazil (Sao Paulo)" + icon = "/emojis/1f1e7-1f1f7.png" + } + "brazilsoutheast" = { + name = "Brazil Southeast" + icon = "/emojis/1f1e7-1f1f7.png" + } + "brazilus" = { + name = "Brazil US" + icon = "/emojis/1f1e7-1f1f7.png" + } + "canada" = { + name = "Canada" + icon = "/emojis/1f1e8-1f1e6.png" + } + "canadacentral" = { + name = "Canada (Toronto)" + icon = "/emojis/1f1e8-1f1e6.png" + } + "canadaeast" = { + name = "Canada East" + icon = "/emojis/1f1e8-1f1e6.png" + } + "centralindia" = { + name = "India (Pune)" + icon = "/emojis/1f1ee-1f1f3.png" + } + "centralus" = { + name = "US (Iowa)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "eastasia" = { + name = "East Asia (Hong Kong)" + icon = "/emojis/1f1f0-1f1f7.png" + } + "eastus" = { + name = "US (Virginia)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "eastus2" = { + name = "US (Virginia) 2" + icon = "/emojis/1f1fa-1f1f8.png" + } + "europe" = { + name = "Europe" + icon = "/emojis/1f30d.png" + } + "france" = { + name = "France" + icon = "/emojis/1f1eb-1f1f7.png" + } + "francecentral" = { + name = "France (Paris)" + icon = "/emojis/1f1eb-1f1f7.png" + } + "francesouth" = { + name = "France South" + icon = "/emojis/1f1eb-1f1f7.png" + } + "germany" = { + name = "Germany" + icon = "/emojis/1f1e9-1f1ea.png" + } + "germanynorth" = { + name = "Germany North" + icon = "/emojis/1f1e9-1f1ea.png" + } + "germanywestcentral" = { + name = "Germany (Frankfurt)" + icon = "/emojis/1f1e9-1f1ea.png" + } + "india" = { + name = "India" + icon = "/emojis/1f1ee-1f1f3.png" + } + "japan" = { + name = "Japan" + icon = "/emojis/1f1ef-1f1f5.png" + } + "japaneast" = { + name = "Japan (Tokyo)" + icon = "/emojis/1f1ef-1f1f5.png" + } + "japanwest" = { + name = "Japan West" + icon = "/emojis/1f1ef-1f1f5.png" + } + "jioindiacentral" = { + name = "Jio India Central" + icon = "/emojis/1f1ee-1f1f3.png" + } + "jioindiawest" = { + name = "Jio India West" + icon = "/emojis/1f1ee-1f1f3.png" + } + "koreacentral" = { + name = "Korea (Seoul)" + icon = "/emojis/1f1f0-1f1f7.png" + } + "koreasouth" = { + name = "Korea South" + icon = "/emojis/1f1f0-1f1f7.png" + } + "northcentralus" = { + name = "North Central US" + icon = "/emojis/1f1fa-1f1f8.png" + } + "northeurope" = { + name = "Europe (Ireland)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "norway" = { + name = "Norway" + icon = "/emojis/1f1f3-1f1f4.png" + } + "norwayeast" = { + name = "Norway (Oslo)" + icon = "/emojis/1f1f3-1f1f4.png" + } + "norwaywest" = { + name = "Norway West" + icon = "/emojis/1f1f3-1f1f4.png" + } + "qatarcentral" = { + name = "Qatar (Doha)" + icon = "/emojis/1f1f6-1f1e6.png" + } + "singapore" = { + name = "Singapore" + icon = "/emojis/1f1f8-1f1ec.png" + } + "southafrica" = { + name = "South Africa" + icon = "/emojis/1f1ff-1f1e6.png" + } + "southafricanorth" = { + name = "South Africa (Johannesburg)" + icon = "/emojis/1f1ff-1f1e6.png" + } + "southafricawest" = { + name = "South Africa West" + icon = "/emojis/1f1ff-1f1e6.png" + } + "southcentralus" = { + name = "US (Texas)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "southeastasia" = { + name = "Southeast Asia (Singapore)" + icon = "/emojis/1f1f0-1f1f7.png" + } + "southindia" = { + name = "South India" + icon = "/emojis/1f1ee-1f1f3.png" + } + "swedencentral" = { + name = "Sweden (Gävle)" + icon = "/emojis/1f1f8-1f1ea.png" + } + "switzerland" = { + name = "Switzerland" + icon = "/emojis/1f1e8-1f1ed.png" + } + "switzerlandnorth" = { + name = "Switzerland (Zurich)" + icon = "/emojis/1f1e8-1f1ed.png" + } + "switzerlandwest" = { + name = "Switzerland West" + icon = "/emojis/1f1e8-1f1ed.png" + } + "uae" = { + name = "United Arab Emirates" + icon = "/emojis/1f1e6-1f1ea.png" + } + "uaecentral" = { + name = "UAE Central" + icon = "/emojis/1f1e6-1f1ea.png" + } + "uaenorth" = { + name = "UAE (Dubai)" + icon = "/emojis/1f1e6-1f1ea.png" + } + "uk" = { + name = "United Kingdom" + icon = "/emojis/1f1ec-1f1e7.png" + } + "uksouth" = { + name = "UK (London)" + icon = "/emojis/1f1ec-1f1e7.png" + } + "ukwest" = { + name = "UK West" + icon = "/emojis/1f1ec-1f1e7.png" + } + "unitedstates" = { + name = "United States" + icon = "/emojis/1f1fa-1f1f8.png" + } + "westcentralus" = { + name = "West Central US" + icon = "/emojis/1f1fa-1f1f8.png" + } + "westeurope" = { + name = "Europe (Netherlands)" + icon = "/emojis/1f1ea-1f1fa.png" + } + "westindia" = { + name = "West India" + icon = "/emojis/1f1ee-1f1f3.png" + } + "westus" = { + name = "West US" + icon = "/emojis/1f1fa-1f1f8.png" + } + "westus2" = { + name = "US (Washington)" + icon = "/emojis/1f1fa-1f1f8.png" + } + "westus3" = { + name = "US (Arizona)" + icon = "/emojis/1f1fa-1f1f8.png" + } + } } data "coder_parameter" "region" { - name = "azure_region" - display_name = var.display_name - description = var.description - default = var.default - mutable = var.mutable - dynamic "option" { - for_each = { for k, v in local.all_regions : k => v if !(contains(var.exclude, k)) } - content { - name = try(var.custom_names[option.key], option.value.name) - icon = try(var.custom_icons[option.key], option.value.icon) - value = option.key - } + name = "azure_region" + display_name = var.display_name + description = var.description + default = var.default + mutable = var.mutable + dynamic "option" { + for_each = { for k, v in local.all_regions : k => v if !(contains(var.exclude, k)) } + content { + name = try(var.custom_names[option.key], option.value.name) + icon = try(var.custom_icons[option.key], option.value.icon) + value = option.key } + } } output "value" { - value = data.coder_parameter.region.value + value = data.coder_parameter.region.value } diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..dfed91948022558cca9901420988b9f7b8ce4291 GIT binary patch literal 1269 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p+vrLKXi zJswMWd=&m(x}P)um;5aTchyRJX^k4AgQn*$r?LPQ0Rc!Y2yj3sINbo{SHTo8 zU7^>QWY?W~^6`e`8NQHL$ArI~nX_f*b-m9byf+zGKXvlW^<5lpqB_~{`2&UiU+W%M zzxQ6qSe59M^FH|3w^}5JeDH*sJZCdZIO28OumArU7@&~|6DEp=nMRaiu%ZLdAiV;m zxonDylJzq4ic1o6a`a#cP%oq?HPueR$Uvbuvnn+|O-I2*Au%U2Jug2Elw`o*-+u@I zalr0?MlK7K=CUa@HnIb1#9{xnipi!HSPI@Jtrk0Gj4 zfevn%uiBua-I|&tz!+z$XP{@m08ja_6wd)qYR{l%!)VkHDo)KUOD)oKttd$?%1g`% zE-A{)OSe-nL|AVDw_X4mrLj;N7$QKI8e(y1No7H5adJ^+K?$fB0P`|RN(zdt^!1BU p(=&@piYoQ;3UafG_413-_2FvtbrEb`L!ifu^-9vKKoK$+0RW`X { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + url: "foo", + }); + + it("fails without git", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "some-url", + }); + const output = await executeScriptInContainer(state, "alpine"); + expect(output.exitCode).toBe(1); + expect(output.stdout).toEqual(["Git is not installed!"]); + }); + + it("runs with git", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "fake-url", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(128); + expect(output.stdout).toEqual([ + "Creating directory ~/fake-url...", + "Cloning fake-url to ~/fake-url...", + ]); + }); +}); diff --git a/git-clone/main.tf b/git-clone/main.tf index e1e9be4..0e0b23e 100644 --- a/git-clone/main.tf +++ b/git-clone/main.tf @@ -10,8 +10,8 @@ terraform { } variable "url" { - description = "The URL of the Git repository." - type = string + description = "The URL of the Git repository." + type = string } variable "path" { @@ -21,17 +21,17 @@ variable "path" { } variable "agent_id" { - description = "The ID of a Coder agent." - type = string + description = "The ID of a Coder agent." + type = string } resource "coder_script" "git_clone" { - agent_id = var.agent_id - display_name = "Git Clone" - icon = "/icons/git.svg" - script = templatefile("${path.module}/run.sh", { - CLONE_PATH: var.path != "" ? var.path : join("/", ["~", basename(var.url)]), - REPO_URL: var.url, - }) - run_on_start = true + agent_id = var.agent_id + display_name = "Git Clone" + icon = "/icons/git.svg" + script = templatefile("${path.module}/run.sh", { + CLONE_PATH : var.path != "" ? var.path : join("/", ["~", basename(var.url)]), + REPO_URL : var.url, + }) + run_on_start = true } diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 7abe4d2..d7fdfb0 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -6,6 +6,7 @@ maintainer_github: coder verified: true tags: [ide, jetbrains, helper, parameter] --- + # JetBrains Gateway This module adds a JetBrains Gateway Button to open any workspace with a single click. diff --git a/jfrog/README.md b/jfrog/README.md index 755d5c8..f5f9670 100644 --- a/jfrog/README.md +++ b/jfrog/README.md @@ -10,4 +10,4 @@ tags: [integration] # JFrog -TODO \ No newline at end of file +TODO diff --git a/package.json b/package.json new file mode 100644 index 0000000..c42b502 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "modules", + "scripts": { + "test": "bun test", + "fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.tf", + "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf" + }, + "devDependencies": { + "bun-types": "^1.0.3" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/personalize/main.tf b/personalize/main.tf index fd88fd5..d3c1e9b 100644 --- a/personalize/main.tf +++ b/personalize/main.tf @@ -10,29 +10,29 @@ terraform { } variable "agent_id" { - type = string - description = "The ID of a Coder agent." + type = string + description = "The ID of a Coder agent." } variable "path" { - type = string - description = "The path to a script that will be ran on start enabling a user to personalize their workspace." - default = "~/personalize" + type = string + description = "The path to a script that will be ran on start enabling a user to personalize their workspace." + default = "~/personalize" } variable "log_path" { - type = string + type = string description = "The path to a log file that will contain the output of the personalize script." - default = "~/personalize.log" + default = "~/personalize.log" } resource "coder_script" "personalize" { - agent_id = var.agent_id - script = templatefile("${path.module}/run.sh", { - PERSONALIZE_PATH: var.path, - }) - display_name = "Personalize" - icon = "/emojis/1f58c.png" - log_path = var.log_path - run_on_start = true + agent_id = var.agent_id + script = templatefile("${path.module}/run.sh", { + PERSONALIZE_PATH : var.path, + }) + display_name = "Personalize" + icon = "/emojis/1f58c.png" + log_path = var.log_path + run_on_start = true } diff --git a/setup.ts b/setup.ts new file mode 100644 index 0000000..c55df43 --- /dev/null +++ b/setup.ts @@ -0,0 +1,47 @@ +import { readableStreamToText, spawn } from "bun"; +import { afterAll, beforeAll } from "bun:test"; + +const removeStatefiles = async () => { + const proc = spawn([ + "find", + ".", + "-type", + "f", + "-name", + "*.tfstate", + "-name", + "*.tfstate.lock.info", + "-delete", + ]); + await proc.exited; +}; + +const removeOldContainers = async () => { + let proc = spawn([ + "docker", + "ps", + "-a", + "-q", + "--filter", + `label=modules-test`, + ]); + let containerIDsRaw = await readableStreamToText(proc.stdout); + let exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(containerIDsRaw); + } + containerIDsRaw = containerIDsRaw.trim(); + if (containerIDsRaw === "") { + return; + } + proc = spawn(["docker", "rm", "-f", ...containerIDsRaw.split("\n")]); + const stdout = await readableStreamToText(proc.stdout); + exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(stdout); + } +}; + +afterAll(async () => { + await Promise.all([removeStatefiles(), removeOldContainers()]); +}); diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..b4c384f --- /dev/null +++ b/test.ts @@ -0,0 +1,212 @@ +import { readableStreamToText, spawn } from "bun"; +import { afterEach, expect, it } from "bun:test"; +import { readFile, unlink } from "fs/promises"; + +export const runContainer = async ( + image: string, + init = "sleep infinity", +): Promise => { + const proc = spawn([ + "docker", + "run", + "--rm", + "-d", + "--label", + "modules-test=true", + "--entrypoint", + "sh", + image, + "-c", + init, + ]); + let containerID = await readableStreamToText(proc.stdout); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(containerID); + } + return containerID.trim(); +}; + +// executeScriptInContainer finds the only "coder_script" +// resource in the given state and runs it in a container. +export const executeScriptInContainer = async ( + state: TerraformState, + image: string, +): Promise<{ + exitCode: number; + stdout: string[]; + stderr: string[]; +}> => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + const resp = await execContainer(id, ["sh", "-c", instance.script]); + const stdout = resp.stdout.trim().split("\n"); + const stderr = resp.stderr.trim().split("\n"); + return { + exitCode: resp.exitCode, + stdout, + stderr, + }; +}; + +export const execContainer = async ( + id: string, + cmd: string[], +): Promise<{ + exitCode: number; + stderr: string; + stdout: string; +}> => { + const proc = spawn(["docker", "exec", id, ...cmd], { + stderr: "pipe", + stdout: "pipe", + }); + const [stderr, stdout] = await Promise.all([ + readableStreamToText(proc.stderr), + readableStreamToText(proc.stdout), + ]); + const exitCode = await proc.exited; + return { + exitCode, + stderr, + stdout, + }; +}; + +export interface TerraformState { + outputs: { + [key: string]: { + type: string; + value: any; + }; + } + resources: [ + { + type: string; + name: string; + provider: string; + instances: [ + { + attributes: { + [key: string]: any; + }; + }, + ]; + }, + ]; +} + +export interface CoderScriptAttributes { + script: string; + agent_id: string; + url: string; +} + +// findResourceInstance finds the first instance of the given resource +// type in the given state. If name is specified, it will only find +// the instance with the given name. +export const findResourceInstance = ( + state: TerraformState, + type: T, + name?: string, + // if type is "coder_script" return CoderScriptAttributes +): T extends "coder_script" + ? CoderScriptAttributes + : Record => { + const resource = state.resources.find( + (resource) => + resource.type === type && (name ? resource.name === name : true), + ); + if (!resource) { + throw new Error(`Resource ${type} not found`); + } + if (resource.instances.length !== 1) { + throw new Error( + `Resource ${type} has ${resource.instances.length} instances`, + ); + } + return resource.instances[0].attributes as any; +}; + +// assertRequiredVariables creates a test-case +// for each variable provided and ensures that +// the apply fails without it. +export const testRequiredVariables = ( + dir: string, + vars: Record, +) => { + // Ensures that all required variables are provided. + 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 = {}; + varNames.forEach((otherVarName) => { + if (otherVarName !== varName) { + localVars[otherVarName] = vars[otherVarName]; + } + }); + try { + await runTerraformApply(dir, localVars); + } catch (ex) { + expect(ex.message).toContain( + `input variable \"${varName}\" is not set, and has no default`, + ); + return; + } + throw new Error(`${varName} is not a required variable!`); + }); + }); +}; + +// runTerraformApply runs terraform apply in the given directory +// with the given variables. It is fine to run in parallel with +// other instances of this function, as it uses a random state file. +export const runTerraformApply = async ( + dir: string, + vars: Record, +): Promise => { + const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; + const env = {}; + Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key])); + const proc = spawn( + [ + "terraform", + "apply", + "-compact-warnings", + "-input=false", + "-auto-approve", + "-state", + stateFile, + ], + { + cwd: dir, + env, + stderr: "pipe", + stdout: "pipe", + }, + ); + const text = await readableStreamToText(proc.stderr); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(text); + } + const content = await readFile(stateFile, "utf8"); + await unlink(stateFile); + return JSON.parse(content); +}; + +// runTerraformInit runs terraform init in the given directory. +export const runTerraformInit = async (dir: string) => { + const proc = spawn(["terraform", "init"], { + cwd: dir, + }); + const text = await readableStreamToText(proc.stdout); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(text); + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..86140a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "types": ["bun-types"] + } +} diff --git a/vscode-desktop/main.tf b/vscode-desktop/main.tf index fb588b9..9c810df 100644 --- a/vscode-desktop/main.tf +++ b/vscode-desktop/main.tf @@ -17,16 +17,16 @@ variable "agent_id" { data "coder_workspace" "me" {} resource "coder_app" "vscode" { - agent_id = var.agent_id - external = true - icon = "/icons/code.svg" - slug = "vscode" - url = join("", [ - "vscode://coder.coder-remote/open?owner=", - data.coder_workspace.me.owner, - "&workspace=", - data.coder_workspace.me.name, - "&token=", - data.coder_workspace.me.owner_session_token, - ]) -} + agent_id = var.agent_id + external = true + icon = "/icons/code.svg" + slug = "vscode" + url = join("", [ + "vscode://coder.coder-remote/open?owner=", + data.coder_workspace.me.owner, + "&workspace=", + data.coder_workspace.me.name, + "&token=", + data.coder_workspace.me.owner_session_token, + ]) +}