diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..9ee005c --- /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@v4 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - run: bun test + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - run: bun fmt:ci 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/.images/jupyterlab.png b/.images/jupyterlab.png new file mode 100644 index 0000000..3a0451c Binary files /dev/null and b/.images/jupyterlab.png differ diff --git a/.images/jupyterlab.webp b/.images/jupyterlab.webp deleted file mode 100644 index d87f7c0..0000000 Binary files a/.images/jupyterlab.webp and /dev/null differ diff --git a/.sample/run.sh b/.sample/run.sh index 88af7ad..79eb123 100755 --- a/.sample/run.sh +++ b/.sample/run.sh @@ -1,17 +1,18 @@ #!/usr/bin/env sh -echo "Instalalting ${MODULE_NAME}..." +BOLD='\033[0;1m' +echo "$${BOLD}Installing MODULE_NAME..." # Add code here # Use varibles from the templatefile function in main.tf # e.g. LOG_PATH, PORT, etc. -echo "Installation comlete!" +echo "🥳 Installation comlete!" -echo "Starting ${MODULE_NAME}..." +echo "👷 Starting MODULE_NAME in background..." # Start the app in here # 1. Use & to run it in background # 2. redirct stdout and stderr to log files ./app >${LOG_PATH} 2>&1 & -echo "Sample app started!" +echo "check logs at ${LOG_PATH}" 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..4c3efdc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,37 +6,21 @@ 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" -} +```shell +# Run tests for a specific module! +$ bun test -t '' ``` -You can also test your module by specifying the source as a git repository: +You can test a module locally by updating the source as follows ```hcl -module "MOUDLE_NAME" { - source = "git::https://github.com//.git//?ref=" -} +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. +> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file 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 0000000..dfed919 Binary files /dev/null and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..7bb903b --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./setup.ts"] \ No newline at end of file diff --git a/code-server/main.tf b/code-server/main.tf index 1608799..87e3036 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -10,59 +10,59 @@ terraform { } variable "agent_id" { - type = string - description = "The ID of a Coder agent." + type = string + description = "The ID of a Coder agent." } variable "extensions" { - type = list(string) - description = "A list of extensions to install." - default = [ ] + type = list(string) + description = "A list of extensions to install." + default = [] } variable "port" { - type = number - description = "The port to run code-server on." - default = 13337 + type = number + description = "The port to run code-server on." + default = 13337 } variable "settings" { - type = map(string) - description = "A map of settings to apply to code-server." - default = {} + type = map(string) + description = "A map of settings to apply to code-server." + default = {} } variable "folder" { - type = string - description = "The folder to open in code-server." - default = "" + type = string + description = "The folder to open in code-server." + default = "" } variable "install_prefix" { - type = string - description = "The prefix to install code-server to." - default = "/tmp/code-server" + type = string + description = "The prefix to install code-server to." + default = "/tmp/code-server" } variable "log_path" { - type = string - description = "The path to log code-server to." - default = "/tmp/code-server.log" + type = string + description = "The path to log code-server to." + default = "/tmp/code-server.log" } resource "coder_script" "code-server" { - agent_id = var.agent_id - display_name = "code-server" - icon = "/icon/code.svg" - script = templatefile("${path.module}/run.sh", { - EXTENSIONS: join(",", var.extensions), - PORT: var.port, - LOG_PATH: var.log_path, - INSTALL_PREFIX: var.install_prefix, - // This is necessary otherwise the quotes are stripped! - SETTINGS: replace(jsonencode(var.settings), "\"", "\\\""), - }) - run_on_start = true + agent_id = var.agent_id + display_name = "code-server" + icon = "/icon/code.svg" + script = templatefile("${path.module}/run.sh", { + EXTENSIONS : join(",", var.extensions), + PORT : var.port, + LOG_PATH : var.log_path, + INSTALL_PREFIX : var.install_prefix, + // This is necessary otherwise the quotes are stripped! + SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), + }) + run_on_start = true } resource "coder_app" "code-server" { diff --git a/dotfiles/README.md b/dotfiles/README.md index 096c74e..316773c 100644 --- a/dotfiles/README.md +++ b/dotfiles/README.md @@ -10,3 +10,10 @@ 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. + +```hcl +module "dotfiles" { + source = "https://registry.coder.com/modules/dotfiles" + agent_id = coder_agent.example.id +} +``` diff --git a/dotfiles/main.tf b/dotfiles/main.tf index c1479eb..d3d0de2 100644 --- a/dotfiles/main.tf +++ b/dotfiles/main.tf @@ -16,11 +16,12 @@ variable "agent_id" { data "coder_parameter" "dotfiles_uri" { type = "string" + name = "dotfiles_uri" display_name = "Dotfiles URL (optional)" default = "" description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace" mutable = true - icon = "https://raw.githubusercontent.com/jglovier/dotfiles-logo/main/dotfiles-logo-icon.svg" + icon = "/icon/dotfiles.svg" } resource "coder_script" "personalize" { @@ -32,6 +33,6 @@ resource "coder_script" "personalize" { fi EOT display_name = "Dotfiles" - icon = "https://raw.githubusercontent.com/jglovier/dotfiles-logo/main/dotfiles-logo-icon.svg" + icon = "/icon/dotfiles.svg" run_on_start = true } diff --git a/fly-region/README.md b/fly-region/README.md index 6cd1f93..03a3502 100644 --- a/fly-region/README.md +++ b/fly-region/README.md @@ -13,6 +13,7 @@ This module adds Fly.io regions to your Coder template. Regions can be whitelist ## Examples + ### Using default settings We can use the simplest format here, only adding a default selection as the `atl` region. @@ -62,3 +63,4 @@ module "fly-region" { ## Associated template For a pre-configured Fly.io template, see the Coder template registry. + diff --git a/gcp-region/README.md b/gcp-region/README.md index 553a8fd..cfa1370 100644 --- a/gcp-region/README.md +++ b/gcp-region/README.md @@ -6,6 +6,7 @@ maintainer_github: coder verified: true tags: [gcp, regions, parameter, helper] --- + # Google Cloud Platform Regions This module adds Google Cloud Platform regions to your Coder template. @@ -16,32 +17,32 @@ This module adds Google Cloud Platform regions to your Coder template. 1. Add only GPU zones in the US West 1 region: - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - default = ["us-west1-a"] - regions = ["us-west1"] - gpu_only = false - } - ``` + ```hcl + module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + default = ["us-west1-a"] + regions = ["us-west1"] + gpu_only = false + } + ``` 2. Add all zones in the Europe West region: - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - regions = ["europe-west"] - single_zone_per_region = false - } - ``` + ```hcl + module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + regions = ["europe-west"] + single_zone_per_region = false + } + ``` 3. Add a single zone from each region in US and Europe that laos has GPUs - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - regions = ["us", "europe"] - gpu_only = true - single_zone_per_region = true - } - ``` + ```hcl + module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + regions = ["us", "europe"] + gpu_only = true + single_zone_per_region = true + } + ``` diff --git a/git-clone/README.md b/git-clone/README.md index b89c47e..40ed231 100644 --- a/git-clone/README.md +++ b/git-clone/README.md @@ -6,6 +6,7 @@ maintainer_github: coder verified: true tags: [git, helper] --- + # Git Clone This module allows you to automatically clone a repository by URL and skip if it exists in the path provided. diff --git a/git-clone/main.test.ts b/git-clone/main.test.ts new file mode 100644 index 0000000..0c3dd54 --- /dev/null +++ b/git-clone/main.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("git-clone", async () => { + 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/jupyterlab/README.md b/jupyterlab/README.md index d551f96..0953737 100644 --- a/jupyterlab/README.md +++ b/jupyterlab/README.md @@ -11,7 +11,7 @@ tags: [jupyter, helper, ide, web] A module that adds JupyterLab in your Coder template. -![JupyterLab](../.images/jupyterlab.webp) +![JupyterLab](../.images/jupyterlab.png) ```hcl module "jupyterlab" { diff --git a/jupyterlab/main.tf b/jupyterlab/main.tf index e95c0a2..1727aa4 100644 --- a/jupyterlab/main.tf +++ b/jupyterlab/main.tf @@ -9,10 +9,6 @@ terraform { } } -locals { - icon_url = "/icon/jupyter.svg" -} - # Add required variables for your modules and remove any unneeded variables variable "agent_id" { type = string @@ -34,13 +30,12 @@ variable "port" { resource "coder_script" "jupyterlab" { agent_id = var.agent_id display_name = "jupyterlab" - icon = local.icon_url + icon = "/icon/jupyter.svg" script = templatefile("${path.module}/run.sh", { LOG_PATH : var.log_path, PORT : var.port }) run_on_start = true - run_on_stopt = false } resource "coder_app" "jupyterlab" { @@ -48,7 +43,7 @@ resource "coder_app" "jupyterlab" { slug = "jupyterlab" display_name = "JupyterLab" url = "http://localhost:${var.port}" - icon = local.icon_url + icon = "/icon/jupyter.svg" subdomain = true share = "owner" } diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh index a3a484a..ec7c28d 100755 --- a/jupyterlab/run.sh +++ b/jupyterlab/run.sh @@ -1,22 +1,25 @@ #!/usr/bin/env sh -echo "Instalalting ${MODULE_NAME}..." +BOLD='\033[0;1m' + +echo "$${BOLD}Installing jupyterlab!\n" # check if jupyterlab is installed -if ! command -v jupyterlab &> /dev/null then - # install jupyterlab +if ! command -v jupyterlab > /dev/null 2>&1; then + # install jupyterlab # check if python3 pip is installed - if ! command -v pip3 &> /dev/null then + if ! command -v pip3 > /dev/null 2>&1; then echo "pip3 is not installed" - echo "Please install pip3 and try again" + echo "Please install pip3 in your Dockerfile/VM image before running this script" exit 1 fi - pip3 install jupyterlab - echo "jupyterlab installed!" + # install jupyterlab + pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyterlab + echo "🥳 jupyterlab has been installed\n\n" +else + echo "🥳 jupyterlab is already installed\n\n" fi -echo "Starting ${MODULE_NAME}..." - -$HOME/.local/bin/jupyter lab --no-browser --LabApp.token='' --LabApp.password='' >${LOG_PATH} 2>&1 & - -echo "Started ${MODULE_NAME}!" +echo "👷 Starting jupyterlab in background..." +echo "check logs at ${LOG_PATH}" +$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 & diff --git a/kasmvnc/README.md b/kasmvnc/README.md deleted file mode 100644 index 0bfcaf9..0000000 --- a/kasmvnc/README.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -display_name: KasmVNC -description: A modern open source VNC server -icon: ../.icons/kasmvnc.svg -maintainer_github: coder -verified: true -tags: [helper, VNC, web] ---- - -# KasmVNC - -Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and create an app to access it via the dashboard. - -## Examples - -1. Add latest version of KasmVNC with [`lxde`](https://www.lxde.org/) desktop environment: - - ```hcl - module "kasmvnc" { - source = "https://registry.coder.com/modules/kasmvnc" - agent_id = coder_agent.example.id - } - - ``` - -2. Add specific version of KasmVNC with [`mate`](https://mate-desktop.org/) desktop environment and custom port: - - ```hcl - module "kasmvnc" { - source = "https://registry.coder.com/modules/kasmvnc" - agent_id = coder_agent.example.id - version = "1.0.0" - desktop_environment = "mate" - port = 6080 - } - - ``` - -![Screenshot of KasmVNC]() //TODO diff --git a/kasmvnc/main.tf b/kasmvnc/main.tf deleted file mode 100644 index 6fd0ba4..0000000 --- a/kasmvnc/main.tf +++ /dev/null @@ -1,55 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - coder = { - source = "coder/coder" - version = ">= 0.12" - } - } -} - -variable "agent_id" { - type = string - description = "The ID of a Coder agent." -} - -variable "port" { - type = number - description = "The port to run KasmVNC on." - default = 8443 -} - -variable "desktop_environment" { - type = string - description = "The desktop environment to for KasmVNC (xfce, lxde, mate, etc)." - default = "lxde" -} - -variable "version" { - type = string - description = "Version of KasmVNC to install." - default = "1.2.0" -} - -resource "coder_script" "kasm_vnc" { - agent_id = var.agent_id - display_name = "KasmVNC" - icon = "/icon/kasmvnc.svg" - script = templatefile("${path.module}/run.sh", { - PORT : var.port, - DESKTOP_ENVIRONMENT : var.desktop_environment, - VERSION : var.version - }) - run_on_start = true -} - -resource "coder_app" "kasm_vnc" { - agent_id = var.agent_id - slug = "kasm-vnc" - display_name = "kasmVNC" - url = "http://localhost:${var.port}" - icon = "/icon/kasmvnc.svg" - subdomain = false - share = "owner" -} diff --git a/kasmvnc/run.sh b/kasmvnc/run.sh deleted file mode 100644 index 7a59580..0000000 --- a/kasmvnc/run.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -# Check if desktop enivronment is installed -if ! dpkg -s ${DESKTOP_ENVIRONMENT} &>/dev/null; then - sudo apt-get update - DEBIAN_FRONTEND=noninteractive sudo apt-get install -y ${DESKTOP_ENVIRONMENT} -else - echo "${DESKTOP_ENVIRONMENT} is already installed." -fi - -# Check if vncserver is installed -if ! dpkg -s kasmvncserver &>/dev/null; then - cd /tmp - wget https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_focal_${VERSION}_amd64.deb - sudo apt install -y ./kasmvncserver_focal_${VERSION}_amd64.deb - printf "🥳 KasmVNC v${VERSION} has been successfully installed!\n\n" -else - echo "KasmVNC is already installed." -fi - -sudo addgroup $USER ssl-cert - -# Coder port-forwarding from dashboard only supports HTTP -sudo bash -c 'cat > /etc/kasmvnc/kasmvnc.yaml < { + const proc = spawn([ + "find", + ".", + "-type", + "f", + "-o", + "-name", + "*.tfstate", + "-o", + "-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/README.md b/vscode-desktop/README.md index c188c45..2651aed 100644 --- a/vscode-desktop/README.md +++ b/vscode-desktop/README.md @@ -12,3 +12,10 @@ tags: [ide, vscode, helper] Add a button to open any workspace with a single click. Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). + +```hcl +module "vscode" { + source = "https://registry.coder.com/modules/vscode-desktop" + agent_id = coder_agent.example.id +} +``` diff --git a/vscode-desktop/main.tf b/vscode-desktop/main.tf index fb588b9..bd7881b 100644 --- a/vscode-desktop/main.tf +++ b/vscode-desktop/main.tf @@ -17,16 +17,17 @@ 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 = "/icon/code.svg" + slug = "vscode" + display_name = "VS Code Desktop" + 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, + ]) +}