From 892174da7c90f11bb6df7206822211ff9f24f30a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 14 Apr 2024 07:10:33 -0700 Subject: [PATCH 1/7] feat(git-config): allow `data.coder_workspace.me.owner_email` to be blank (#222) --- git-config/main.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++ git-config/main.tf | 2 ++ test.ts | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 git-config/main.test.ts diff --git a/git-config/main.test.ts b/git-config/main.test.ts new file mode 100644 index 0000000..1241956 --- /dev/null +++ b/git-config/main.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("git-config", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("can run apply allow_username_change and allow_email_change disabled", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + allow_username_change: "false", + allow_email_change: "false", + }); + + const resources = state.resources; + expect(resources).toHaveLength(3); + expect(resources).toMatchObject([ + { type: "coder_workspace", name: "me" }, + { type: "coder_env", name: "git_author_name" }, + { type: "coder_env", name: "git_commmiter_name" }, + ]); + }); + + it("can run apply allow_email_change enabled", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + allow_email_change: "true", + }); + + const resources = state.resources; + expect(resources).toHaveLength(5); + expect(resources).toMatchObject([ + { type: "coder_parameter", name: "user_email" }, + { type: "coder_parameter", name: "username" }, + { type: "coder_workspace", name: "me" }, + { type: "coder_env", name: "git_author_name" }, + { type: "coder_env", name: "git_commmiter_name" }, + ]); + }); + + it("can run apply allow_email_change enabled", async () => { + const state = await runTerraformApply( + import.meta.dir, + { + agent_id: "foo", + allow_username_change: "false", + allow_email_change: "false", + }, + { CODER_WORKSPACE_OWNER_EMAIL: "foo@emai.com" }, + ); + + const resources = state.resources; + expect(resources).toHaveLength(5); + expect(resources).toMatchObject([ + { type: "coder_workspace", name: "me" }, + { type: "coder_env", name: "git_author_email" }, + { type: "coder_env", name: "git_author_name" }, + { type: "coder_env", name: "git_commmiter_email" }, + { type: "coder_env", name: "git_commmiter_name" }, + ]); + }); +}); diff --git a/git-config/main.tf b/git-config/main.tf index d92a0b7..a0e96ad 100644 --- a/git-config/main.tf +++ b/git-config/main.tf @@ -65,10 +65,12 @@ resource "coder_env" "git_author_email" { agent_id = var.agent_id name = "GIT_AUTHOR_EMAIL" value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email) + count = data.coder_workspace.me.owner_email != "" ? 1 : 0 } resource "coder_env" "git_commmiter_email" { agent_id = var.agent_id name = "GIT_COMMITTER_EMAIL" value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email) + count = data.coder_workspace.me.owner_email != "" ? 1 : 0 } diff --git a/test.ts b/test.ts index 37e0805..97416cf 100644 --- a/test.ts +++ b/test.ts @@ -171,9 +171,9 @@ export const testRequiredVariables = ( export const runTerraformApply = async ( dir: string, vars: Record, + env: Record = {}, ): Promise => { const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; - const env = {}; Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key])); const proc = spawn( [ From c4df384f4b366f947ab0460118255d7096965158 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 14 Apr 2024 07:14:47 -0700 Subject: [PATCH 2/7] feat(code-server): add extension_dir variable (#205) --- code-server/main.tf | 7 +++++++ code-server/run.sh | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/code-server/main.tf b/code-server/main.tf index 30b92bc..8c0f1e0 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -95,6 +95,12 @@ variable "use_cached" { default = false } +variable "extensions_dir" { + type = string + description = "Override the directory to store extensions in." + default = "" +} + resource "coder_script" "code-server" { agent_id = var.agent_id display_name = "code-server" @@ -110,6 +116,7 @@ resource "coder_script" "code-server" { SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), OFFLINE : var.offline, USE_CACHED : var.use_cached, + EXTENSIONS_DIR : var.extensions_dir, }) run_on_start = true diff --git a/code-server/run.sh b/code-server/run.sh index 2444324..b04e131 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -6,10 +6,16 @@ CODE='\033[36;40;1m' RESET='\033[0m' CODE_SERVER="${INSTALL_PREFIX}/bin/code-server" +# Set extension directory +EXTENSION_ARG="" +if [ -n "${EXTENSIONS_DIR}" ]; then + EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}" +fi + function run_code_server() { echo "👷 Running code-server in the background..." echo "Check logs at ${LOG_PATH}!" - $CODE_SERVER --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 & + $CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 & } # Check if the settings file exists... @@ -57,7 +63,7 @@ for extension in "$${EXTENSIONLIST[@]}"; do continue fi printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" - output=$($CODE_SERVER --install-extension "$extension") + output=$($CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension") if [ $? -ne 0 ]; then echo "Failed to install extension: $extension: $output" exit 1 From a8c659ad6f7474e1f056f49c3d3599c260b38d37 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 15 Apr 2024 10:31:21 -0700 Subject: [PATCH 3/7] feat: add coder_parameter_order to all data.coder_parameter fields (#223) --- aws-region/main.test.ts | 9 +++++++++ aws-region/main.tf | 7 +++++++ azure-region/main.test.ts | 9 +++++++++ azure-region/main.tf | 7 +++++++ dotfiles/main.test.ts | 10 ++++++++++ dotfiles/main.tf | 7 +++++++ exoscale-instance-type/main.test.ts | 9 +++++++++ exoscale-instance-type/main.tf | 7 +++++++ exoscale-zone/main.test.ts | 9 +++++++++ exoscale-zone/main.tf | 6 ++++++ gcp-region/main.test.ts | 9 +++++++++ gcp-region/main.tf | 7 +++++++ git-config/main.test.ts | 30 +++++++++++++++++++++++++++++ git-config/main.tf | 7 +++++++ 14 files changed, 133 insertions(+) diff --git a/aws-region/main.test.ts b/aws-region/main.test.ts index 42433df..0693e65 100644 --- a/aws-region/main.test.ts +++ b/aws-region/main.test.ts @@ -22,4 +22,13 @@ describe("aws-region", async () => { }); expect(state.outputs.value.value).toBe("us-west-2"); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(1); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/aws-region/main.tf b/aws-region/main.tf index 7594320..12a01fe 100644 --- a/aws-region/main.tf +++ b/aws-region/main.tf @@ -51,6 +51,12 @@ variable "exclude" { type = list(string) } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + locals { # This is a static list because the regions don't change _that_ # frequently and including the `aws_regions` data source requires @@ -176,6 +182,7 @@ data "coder_parameter" "region" { display_name = var.display_name description = var.description default = var.default == "" ? null : var.default + order = var.coder_parameter_order mutable = var.mutable dynamic "option" { for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } diff --git a/azure-region/main.test.ts b/azure-region/main.test.ts index 0e41e29..bebc0c9 100644 --- a/azure-region/main.test.ts +++ b/azure-region/main.test.ts @@ -22,4 +22,13 @@ describe("azure-region", async () => { }); expect(state.outputs.value.value).toBe("westus"); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(1); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/azure-region/main.tf b/azure-region/main.tf index 307d61d..3d1c2f1 100644 --- a/azure-region/main.tf +++ b/azure-region/main.tf @@ -50,6 +50,12 @@ variable "exclude" { type = list(string) } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + locals { # Note: Options are limited to 64 regions, some redundant regions have been removed. all_regions = { @@ -309,6 +315,7 @@ data "coder_parameter" "region" { display_name = var.display_name description = var.description default = var.default == "" ? null : var.default + order = var.coder_parameter_order mutable = var.mutable icon = "/icon/azure.png" dynamic "option" { diff --git a/dotfiles/main.test.ts b/dotfiles/main.test.ts index 175690f..6026719 100644 --- a/dotfiles/main.test.ts +++ b/dotfiles/main.test.ts @@ -27,4 +27,14 @@ describe("dotfiles", async () => { }); expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(2); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/dotfiles/main.tf b/dotfiles/main.tf index 12ca826..cf21864 100644 --- a/dotfiles/main.tf +++ b/dotfiles/main.tf @@ -20,10 +20,17 @@ variable "default_dotfiles_uri" { default = "" } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + data "coder_parameter" "dotfiles_uri" { type = "string" name = "dotfiles_uri" display_name = "Dotfiles URL (optional)" + order = var.coder_parameter_order default = var.default_dotfiles_uri description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace" mutable = true diff --git a/exoscale-instance-type/main.test.ts b/exoscale-instance-type/main.test.ts index eeb6745..e4b998b 100644 --- a/exoscale-instance-type/main.test.ts +++ b/exoscale-instance-type/main.test.ts @@ -31,4 +31,13 @@ describe("exoscale-instance-type", async () => { }); }).toThrow('default value "gpu3.huge" must be defined as one of options'); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(1); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/exoscale-instance-type/main.tf b/exoscale-instance-type/main.tf index f7c8998..65d3729 100644 --- a/exoscale-instance-type/main.tf +++ b/exoscale-instance-type/main.tf @@ -56,6 +56,12 @@ variable "exclude" { type = list(string) } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + locals { # https://www.exoscale.com/pricing/ @@ -257,6 +263,7 @@ data "coder_parameter" "instance_type" { display_name = var.display_name description = var.description default = var.default == "" ? null : var.default + order = var.coder_parameter_order mutable = var.mutable dynamic "option" { for_each = [for k, v in concat( diff --git a/exoscale-zone/main.test.ts b/exoscale-zone/main.test.ts index 7c423e7..ca8eeb7 100644 --- a/exoscale-zone/main.test.ts +++ b/exoscale-zone/main.test.ts @@ -22,4 +22,13 @@ describe("exoscale-zone", async () => { }); expect(state.outputs.value.value).toBe("at-vie-1"); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(1); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/exoscale-zone/main.tf b/exoscale-zone/main.tf index 01f1467..090acb4 100644 --- a/exoscale-zone/main.tf +++ b/exoscale-zone/main.tf @@ -51,6 +51,11 @@ variable "exclude" { type = list(string) } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} locals { # This is a static list because the zones don't change _that_ @@ -94,6 +99,7 @@ data "coder_parameter" "zone" { display_name = var.display_name description = var.description default = var.default == "" ? null : var.default + order = var.coder_parameter_order mutable = var.mutable dynamic "option" { for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) } diff --git a/gcp-region/main.test.ts b/gcp-region/main.test.ts index 2ec623b..bf01c2b 100644 --- a/gcp-region/main.test.ts +++ b/gcp-region/main.test.ts @@ -40,4 +40,13 @@ describe("gcp-region", async () => { }); expect(state.outputs.value.value).toBe("us-west2-b"); }); + + it("set custom order for coder_parameter", async () => { + const order = 99; + const state = await runTerraformApply(import.meta.dir, { + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(1); + expect(state.resources[0].instances[0].attributes.order).toBe(order); + }); }); diff --git a/gcp-region/main.tf b/gcp-region/main.tf index e9f549d..0a75924 100644 --- a/gcp-region/main.tf +++ b/gcp-region/main.tf @@ -63,6 +63,12 @@ variable "single_zone_per_region" { type = bool } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} + locals { zones = { # US Central @@ -715,6 +721,7 @@ data "coder_parameter" "region" { icon = "/icon/gcp.png" mutable = var.mutable default = var.default != null && var.default != "" && (!var.gpu_only || try(local.zones[var.default].gpu, false)) ? var.default : null + order = var.coder_parameter_order dynamic "option" { for_each = { for k, v in local.zones : k => v diff --git a/git-config/main.test.ts b/git-config/main.test.ts index 1241956..fe410aa 100644 --- a/git-config/main.test.ts +++ b/git-config/main.test.ts @@ -66,4 +66,34 @@ describe("git-config", async () => { { type: "coder_env", name: "git_commmiter_name" }, ]); }); + + it("set custom order for coder_parameter for both fields", async () => { + const order = 20; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + allow_username_change: "true", + allow_email_change: "true", + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(5); + // user_email order is the same as the order + expect(state.resources[0].instances[0].attributes.order).toBe(order); + // username order is incremented by 1 + // @ts-ignore: Object is possibly 'null'. + expect(state.resources[1].instances[0]?.attributes.order).toBe(order + 1); + }); + + it("set custom order for coder_parameter for just username", async () => { + const order = 30; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + allow_email_change: "false", + allow_username_change: "true", + coder_parameter_order: order.toString(), + }); + expect(state.resources).toHaveLength(4); + // user_email was not created + // username order is incremented by 1 + expect(state.resources[0].instances[0].attributes.order).toBe(order + 1); + }); }); diff --git a/git-config/main.tf b/git-config/main.tf index a0e96ad..fe19288 100644 --- a/git-config/main.tf +++ b/git-config/main.tf @@ -26,6 +26,11 @@ variable "allow_email_change" { default = false } +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} data "coder_workspace" "me" {} @@ -34,6 +39,7 @@ data "coder_parameter" "user_email" { name = "user_email" type = "string" default = "" + order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null description = "Git user.email to be used for commits. Leave empty to default to Coder user's email." display_name = "Git config user.email" mutable = true @@ -44,6 +50,7 @@ data "coder_parameter" "username" { name = "username" type = "string" default = "" + order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name." display_name = "Full Name for Git config" mutable = true From ed16ba59a95f1d015337cd576edb3961fde691db Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 15 Apr 2024 20:31:32 +0300 Subject: [PATCH 4/7] fix(dotfiles): fix typo and remove a less useful output (#225) --- dotfiles/README.md | 10 +++++----- dotfiles/main.tf | 5 ----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/dotfiles/README.md b/dotfiles/README.md index 2939025..9f8a0fe 100644 --- a/dotfiles/README.md +++ b/dotfiles/README.md @@ -21,13 +21,13 @@ module "dotfiles" { ## Setting a default dotfiles repository -You can set a default dotfiles repository for all users by setting the `default_dotfiles_repo` variable: +You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable: ```tf module "dotfiles" { - source = "registry.coder.com/modules/dotfiles/coder" - version = "1.0.12" - agent_id = coder_agent.example.id - default_dotfiles_repo = "https://github.com/coder/dotfiles" + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.12" + agent_id = coder_agent.example.id + default_dotfiles_uri = "https://github.com/coder/dotfiles" } ``` diff --git a/dotfiles/main.tf b/dotfiles/main.tf index cf21864..ac7f7e6 100644 --- a/dotfiles/main.tf +++ b/dotfiles/main.tf @@ -54,8 +54,3 @@ output "dotfiles_uri" { description = "Dotfiles URI" value = data.coder_parameter.dotfiles_uri.value } - -output "dotfiles_default_uri" { - description = "Dotfiles Default URI" - value = var.default_dotfiles_uri -} \ No newline at end of file From d8f71e4571fd562a7a9ec9c62c9d1338562032d4 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 17 Apr 2024 11:05:04 +0300 Subject: [PATCH 5/7] feat(jetbrains-gateway): Allow fetching latest version dynamically (#226) --- jetbrains-gateway/main.tf | 72 +++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index 7c57bee..4c174a2 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -6,6 +6,10 @@ terraform { source = "coder/coder" version = ">= 0.17" } + http = { + source = "hashicorp/http" + version = ">= 3.0" + } } } @@ -46,6 +50,22 @@ variable "coder_parameter_order" { default = null } +variable "latest" { + type = bool + description = "Whether to fetch the latest version of the IDE." + default = false +} + +variable "channel" { + type = string + description = "JetBrains IDE release channel. Valid values are release and eap." + default = "release" + validation { + condition = can(regex("^(release|eap)$", var.channel)) + error_message = "The channel must be either release or eap." + } +} + variable "jetbrains_ide_versions" { type = map(object({ build_number = string @@ -120,6 +140,11 @@ variable "jetbrains_ides" { } } +data "http" "jetbrains_ide_versions" { + for_each = var.latest ? toset(var.jetbrains_ides) : toset([]) + url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}" +} + locals { jetbrains_ides = { "GO" = { @@ -128,6 +153,7 @@ locals { identifier = "GO", build_number = var.jetbrains_ide_versions["GO"].build_number, download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz" + version = var.jetbrains_ide_versions["GO"].version }, "WS" = { icon = "/icon/webstorm.svg", @@ -135,6 +161,7 @@ locals { identifier = "WS", build_number = var.jetbrains_ide_versions["WS"].build_number, download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz" + version = var.jetbrains_ide_versions["WS"].version }, "IU" = { icon = "/icon/intellij.svg", @@ -142,6 +169,7 @@ locals { identifier = "IU", build_number = var.jetbrains_ide_versions["IU"].build_number, download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz" + version = var.jetbrains_ide_versions["IU"].version }, "PY" = { icon = "/icon/pycharm.svg", @@ -149,6 +177,7 @@ locals { identifier = "PY", build_number = var.jetbrains_ide_versions["PY"].build_number, download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz" + version = var.jetbrains_ide_versions["PY"].version }, "CL" = { icon = "/icon/clion.svg", @@ -156,6 +185,7 @@ locals { identifier = "CL", build_number = var.jetbrains_ide_versions["CL"].build_number, download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz" + version = var.jetbrains_ide_versions["CL"].version }, "PS" = { icon = "/icon/phpstorm.svg", @@ -163,6 +193,7 @@ locals { identifier = "PS", build_number = var.jetbrains_ide_versions["PS"].build_number, download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz" + version = var.jetbrains_ide_versions["PS"].version }, "RM" = { icon = "/icon/rubymine.svg", @@ -170,6 +201,7 @@ locals { identifier = "RM", build_number = var.jetbrains_ide_versions["RM"].build_number, download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz" + version = var.jetbrains_ide_versions["RM"].version } "RD" = { icon = "/icon/rider.svg", @@ -177,8 +209,18 @@ locals { identifier = "RD", build_number = var.jetbrains_ide_versions["RD"].build_number, download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz" + version = var.jetbrains_ide_versions["RD"].version } } + + icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg") + json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {} + key = var.latest ? keys(local.json_data)[0] : "" + display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name + identifier = data.coder_parameter.jetbrains_ide.value + download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number + version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version } data "coder_parameter" "jetbrains_ide" { @@ -193,9 +235,9 @@ data "coder_parameter" "jetbrains_ide" { dynamic "option" { for_each = var.jetbrains_ides content { - icon = lookup(local.jetbrains_ides, option.value).icon - name = lookup(local.jetbrains_ides, option.value).name - value = lookup(local.jetbrains_ides, option.value).identifier + icon = local.jetbrains_ides[option.value].icon + name = local.jetbrains_ides[option.value].name + value = option.value } } } @@ -205,8 +247,8 @@ data "coder_workspace" "me" {} resource "coder_app" "gateway" { agent_id = var.agent_id slug = "gateway" - display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE") - icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg") + display_name = try(lookup(data.coder_parameter.jetbrains_ide.option, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE") + icon = try(lookup(data.coder_parameter.jetbrains_ide.option, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg") external = true order = var.order url = join("", [ @@ -221,36 +263,36 @@ resource "coder_app" "gateway" { "&token=", "$SESSION_TOKEN", "&ide_product_code=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier, + data.coder_parameter.jetbrains_ide.value, "&ide_build_number=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number, + local.build_number, "&ide_download_link=", - local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + local.download_link, ]) } output "identifier" { - value = data.coder_parameter.jetbrains_ide.value + value = local.identifier } -output "name" { - value = coder_app.gateway.display_name +output "display_name" { + value = local.display_name } output "icon" { - value = coder_app.gateway.icon + value = local.icon } output "download_link" { - value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link + value = local.download_link } output "build_number" { - value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number + value = local.build_number } output "version" { - value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version + value = local.version } output "url" { From 43304e5d4eff25088e27e01b1576f7863a778b74 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Wed, 17 Apr 2024 11:27:49 +0300 Subject: [PATCH 6/7] docs(jetbrains-gateway): add examples on how to use the latest version (#228) --- jetbrains-gateway/README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 629afb2..cf51216 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -27,7 +27,7 @@ module "jetbrains_gateway" { ## Examples -### Add GoLand and WebStorm with the default set to GoLand +### Add GoLand and WebStorm as options with the default set to GoLand ```tf module "jetbrains_gateway" { @@ -41,6 +41,37 @@ module "jetbrains_gateway" { } ``` +### Use the latest release version + +```tf +module "jetbrains_gateway" { + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + agent_name = "example" + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true +} +``` + +### Use the latest EAP version + +```tf +module "jetbrains_gateway" { + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + agent_name = "example" + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true + channel = "eap" +} +``` + ## Supported IDEs This module and JetBrains Gateway support the following JetBrains IDEs: From 8766c670e60a1f58b0f80177c017a2d11b54e605 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 17 Apr 2024 12:40:47 -0700 Subject: [PATCH 7/7] feat(git-clone): add support for tree github or gitlab clone url (#210) --- git-clone/README.md | 103 ++++++++++++++++++++++ git-clone/main.test.ts | 192 +++++++++++++++++++++++++++++++++++++++++ git-clone/main.tf | 74 +++++++++++++++- git-clone/run.sh | 10 ++- 4 files changed, 374 insertions(+), 5 deletions(-) diff --git a/git-clone/README.md b/git-clone/README.md index 054e30c..a8dd312 100644 --- a/git-clone/README.md +++ b/git-clone/README.md @@ -50,3 +50,106 @@ data "coder_git_auth" "github" { id = "github" } ``` + +## GitHub clone with branch name + +To GitHub clone with a specific branch like `feat/example` + +```tf +# Prompt the user for the git repo URL +data "coder_parameter" "git_repo" { + name = "git_repo" + display_name = "Git repository" + default = "https://github.com/coder/coder/tree/feat/example" +} + +# Clone the repository for branch `feat/example` +module "git_clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + url = data.coder_parameter.git_repo.value +} + +# Create a code-server instance for the cloned repository +module "code-server" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + order = 1 + folder = "/home/${local.username}/${module.git_clone.folder_name}" +} + +# Create a Coder app for the website +resource "coder_app" "website" { + agent_id = coder_agent.example.id + order = 2 + slug = "website" + external = true + display_name = module.git_clone.folder_name + url = module.git_clone.web_url + icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg" + count = module.git_clone.web_url != "" ? 1 : 0 +} +``` + +Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + url = "https://github.example.com/coder/coder/tree/feat/example" + git_providers = { + "https://github.example.com/" = { + provider = "github" + } + } +} +``` + +## GitLab clone with branch name + +To GitLab clone with a specific branch like `feat/example` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + url = "https://gitlab.com/coder/coder/-/tree/feat/example" +} +``` + +Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com` + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + url = "https://gitlab.example.com/coder/coder/-/tree/feat/example" + git_providers = { + "https://gitlab.example.com/" = { + provider = "gitlab" + } + } +} +``` + +## Git clone with branch_name set + +Alternatively, you can set the `branch_name` attribute to clone a specific branch. + +For example, to clone the `feat/example` branch: + +```tf +module "git-clone" { + source = "registry.coder.com/modules/git-clone/coder" + version = "1.0.11" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" + branch_name = "feat/example" +} +``` diff --git a/git-clone/main.test.ts b/git-clone/main.test.ts index 0c3dd54..87b0e4a 100644 --- a/git-clone/main.test.ts +++ b/git-clone/main.test.ts @@ -36,4 +36,196 @@ describe("git-clone", async () => { "Cloning fake-url to ~/fake-url...", ]); }); + + it("repo_dir should match repo name for https", async () => { + const url = "https://github.com/coder/coder.git"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.folder_name.value).toEqual("coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("repo_dir should match repo name for https without .git", async () => { + const url = "https://github.com/coder/coder"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("repo_dir should match repo name for ssh", async () => { + const url = "git@github.com:coder/coder.git"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.git_provider.value).toEqual(""); + expect(state.outputs.clone_url.value).toEqual(url); + const https_url = "https://github.com/coder/coder.git"; + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("branch_name should not include query string", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch?ref_type=heads", + }); + expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log"); + expect(state.outputs.folder_name.value).toEqual("repo-tests.log"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("branch_name should not include fragments", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch#name", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("gitlab url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + expect(state.outputs.git_provider.value).toEqual("gitlab"); + const https_url = "https://gitlab.com/mike.brew/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("github url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch", + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log"); + expect(state.outputs.git_provider.value).toEqual("github"); + const https_url = "https://github.com/michaelbrewer/repo-tests.log"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/branch"); + }); + + it("self-host git url with branch should match", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url: "https://git.example.com/example/project/-/tree/feat/example", + git_providers: ` + { + "https://git.example.com/" = { + provider = "gitlab" + } + }`, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/project"); + expect(state.outputs.git_provider.value).toEqual("gitlab"); + const https_url = "https://git.example.com/example/project"; + expect(state.outputs.clone_url.value).toEqual(https_url); + expect(state.outputs.web_url.value).toEqual(https_url); + expect(state.outputs.branch_name.value).toEqual("feat/example"); + }); + + it("handle unsupported git provider configuration", async () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "foo", + git_providers: ` + { + "https://git.example.com/" = { + provider = "bitbucket" + } + }`, + }); + }; + expect(t).toThrow('Allowed values for provider are "github" or "gitlab".'); + }); + + it("handle unknown git provider url", async () => { + const url = "https://git.unknown.com/coder/coder"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + base_dir: "/tmp", + url, + }); + expect(state.outputs.repo_dir.value).toEqual("/tmp/coder"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(""); + }); + + it("runs with github clone with switch to feat/branch", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); + + it("runs with gitlab clone with switch to feat/branch", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); + + it("runs with github clone with branch_name set to feat/branch", async () => { + const url = "https://github.com/michaelbrewer/repo-tests.log"; + const branch_name = "feat/branch"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + url, + branch_name, + }); + expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log"); + expect(state.outputs.clone_url.value).toEqual(url); + expect(state.outputs.web_url.value).toEqual(url); + expect(state.outputs.branch_name.value).toEqual(branch_name); + + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "Creating directory ~/repo-tests.log...", + "Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...", + ]); + }); }); diff --git a/git-clone/main.tf b/git-clone/main.tf index c1e65cf..4af5000 100644 --- a/git-clone/main.tf +++ b/git-clone/main.tf @@ -25,8 +25,50 @@ variable "agent_id" { type = string } +variable "git_providers" { + type = map(object({ + provider = string + })) + description = "A mapping of URLs to their git provider." + default = { + "https://github.com/" = { + provider = "github" + }, + "https://gitlab.com/" = { + provider = "gitlab" + }, + } + validation { + error_message = "Allowed values for provider are \"github\" or \"gitlab\"." + condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)]) + } +} + +variable "branch_name" { + description = "The branch name to clone. If not provided, the default branch will be cloned." + type = string + default = "" +} + locals { - clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")]) + # Remove query parameters and fragments from the URL + url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "") + + # Find the git provider based on the URL and determine the tree path + provider_key = try(one([for key in keys(var.git_providers) : key if startswith(local.url, key)]), null) + provider = try(lookup(var.git_providers, local.provider_key).provider, "") + tree_path = local.provider == "gitlab" ? "/-/tree/" : local.provider == "github" ? "/tree/" : "" + + # Remove tree and branch name from the URL + clone_url = var.branch_name == "" && local.tree_path != "" ? replace(local.url, "/${local.tree_path}.*/", "") : local.url + # Extract the branch name from the URL + branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name + # Extract the folder name from the URL + folder_name = replace(basename(local.clone_url), ".git", "") + # Construct the path to clone the repository + clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name]) + # Construct the web URL + web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url } output "repo_dir" { @@ -34,11 +76,37 @@ output "repo_dir" { description = "Full path of cloned repo directory" } +output "git_provider" { + value = local.provider + description = "The git provider of the repository" +} + +output "folder_name" { + value = local.folder_name + description = "The name of the folder that will be created" +} + +output "clone_url" { + value = local.clone_url + description = "The exact Git repository URL that will be cloned" +} + +output "web_url" { + value = local.web_url + description = "Git https repository URL (may be invalid for unsupported providers)" +} + +output "branch_name" { + value = local.branch_name + description = "Git branch name (may be empty)" +} + resource "coder_script" "git_clone" { agent_id = var.agent_id script = templatefile("${path.module}/run.sh", { - CLONE_PATH = local.clone_path - REPO_URL : var.url, + CLONE_PATH = local.clone_path, + REPO_URL : local.clone_url, + BRANCH_NAME : local.branch_name, }) display_name = "Git Clone" icon = "/icon/git.svg" diff --git a/git-clone/run.sh b/git-clone/run.sh index df647a1..bd80717 100755 --- a/git-clone/run.sh +++ b/git-clone/run.sh @@ -2,6 +2,7 @@ REPO_URL="${REPO_URL}" CLONE_PATH="${CLONE_PATH}" +BRANCH_NAME="${BRANCH_NAME}" # Expand home if it's specified! CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" @@ -33,8 +34,13 @@ fi # Check if the directory is empty # and if it is, clone the repo, otherwise skip cloning if [ -z "$(ls -A "$CLONE_PATH")" ]; then - echo "Cloning $REPO_URL to $CLONE_PATH..." - git clone "$REPO_URL" "$CLONE_PATH" + if [ -z "$BRANCH_NAME" ]; then + echo "Cloning $REPO_URL to $CLONE_PATH..." + git clone "$REPO_URL" "$CLONE_PATH" + else + echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..." + git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH" + fi else echo "$CLONE_PATH already exists and isn't empty, skipping clone!" exit 0