diff --git a/.images/jfrog.png b/.images/jfrog.png new file mode 100644 index 0000000..330dad2 Binary files /dev/null and b/.images/jfrog.png differ diff --git a/.sample/main.tf b/.sample/main.tf index 5dcaf01..733f121 100644 --- a/.sample/main.tf +++ b/.sample/main.tf @@ -61,7 +61,7 @@ resource "coder_script" "MODULE_NAME" { LOG_PATH : var.log_path, }) run_on_start = true - run_on_stopt = false + run_on_stop = false } resource "coder_app" "MODULE_NAME" { diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index d7fdfb0..8ac7f41 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -11,6 +11,16 @@ tags: [ide, jetbrains, helper, parameter] This module adds a JetBrains Gateway Button to open any workspace with a single click. +```hcl +module "jetbrains_gateway" { + source = "https://registry.coder.com/modules/jetbrains-gateway" + agent_id = coder_agent.example.id + agent_name = "example" + project_directory = "/home/coder/example" + jetbrains_ides = ["GO", "WS", "IU", "IC", "PY", "PC", "PS", "CL", "RM", "DB", "RD"] +} +``` + ![JetBrains Gateway IDes list](../.images/jetbrains-gateway.png) ## Examples diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index 9e01cf0..1b01242 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -25,6 +25,7 @@ variable "project_directory" { } variable "default" { + default = null type = string description = "Default IDE" } @@ -40,6 +41,16 @@ variable "jetbrains_ides" { ) error_message = "The jetbrains_ides must be a list of valid product codes. https://plugins.jetbrains.com/docs/marketplace/product-codes.html" } + # check if the list is empty + validation { + condition = length(var.jetbrains_ides) > 0 + error_message = "The jetbrains_ides must not be empty." + } + #ccheck if the list contains duplicates + validation { + condition = length(var.jetbrains_ides) == length(set(var.jetbrains_ides)) + error_message = "The jetbrains_ides must not contain duplicates." + } } locals { @@ -108,7 +119,8 @@ data "coder_parameter" "jetbrains_ide" { display_name = "JetBrains IDE" icon = "/icon/gateway.svg" mutable = true - default = var.default != null && var.default != "" ? local.jetbrains_ides[var.default].value : null + # check if default is in the jet_brains_ides list and if it is not empty or null otherwise set it to null + default = contains(var.jetbrains_ides.keys, var.default) && var.default != null && var.default != "" ? var.default : null dynamic "option" { for_each = { for key, value in local.jetbrains_ides : key => value if contains(var.jetbrains_ides, key) } @@ -126,9 +138,26 @@ resource "coder_app" "gateway" { agent_id = var.agent_id display_name = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].name slug = "gateway" - url = "jetbrains-gateway://connect#type=coder&workspace=${data.coder_workspace.me.name}&agent=${var.agent_name}&folder=${var.project_directory}&url=${data.coder_workspace.me.access_url}&token=${data.coder_workspace.me.owner_session_token}&ide_product_code=${jsondecode(data.coder_parameter.jetbrains_ide.value)[0]}&ide_build_number=${jsondecode(data.coder_parameter.jetbrains_ide.value)[1]}&ide_download_link=${jsondecode(data.coder_parameter.jetbrains_ide.value)[2]}" icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon external = true + url = join("", [ + "jetbrains-gateway://connect#type=coder&workspace=", + data.coder_workspace.me.name, + "&agent=", + var.agent_name, + "&folder=", + var.project_directory, + "&url=", + data.coder_workspace.me.access_url, + "&token=", + "$SESSION_TOKEN", + "&ide_product_code=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[0], + "&ide_build_number=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[1], + "&ide_download_link=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[2] + ]) } output "jetbrains_ides" { diff --git a/jfrog/README.md b/jfrog/README.md index f5f9670..cba9e06 100644 --- a/jfrog/README.md +++ b/jfrog/README.md @@ -10,4 +10,35 @@ tags: [integration] # JFrog -TODO +Install the JF CLI and authenticate package managers with Artifactory. + +![JFrog](../.images/jfrog.png) + +## Examples + +### Configure npm, go, and pypi to use Artifactory + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog" + agent_id = coder_agent.example.id + jfrog_url = "https://YYYY.jfrog.io" + artifactory_access_token = var.artifactory_access_token # An admin access token + package_managers = { + "npm": "npm-local", + "go": "go-local", + "pypi": "pypi-local" + } +} +``` + +## Authentication + +Get a JFrog access token from your Artifactory instance. The token must have admin permissions. It is recommended to store the token in a secret terraform variable. + +```hcl +variable "artifactory_access_token" { + type = string + sensitive = true +} +``` diff --git a/jfrog/main.test.ts b/jfrog/main.test.ts new file mode 100644 index 0000000..82ad38b --- /dev/null +++ b/jfrog/main.test.ts @@ -0,0 +1,41 @@ +import { serve } from "bun"; +import { describe } from "bun:test"; +import { + createJSONResponse, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("jfrog", async () => { + await runTerraformInit(import.meta.dir); + + // Run a fake JFrog server so the provider can initialize + // correctly. This saves us from having to make remote requests! + const fakeFrogHost = serve({ + fetch: (req) => { + const url = new URL(req.url); + // See https://jfrog.com/help/r/jfrog-rest-apis/license-information + if (url.pathname === "/artifactory/api/system/license") + return createJSONResponse({ + type: "Commercial", + licensedTo: "JFrog inc.", + validThrough: "May 15, 2036", + }); + if (url.pathname === "/access/api/v1/tokens") + return createJSONResponse({ + token_id: "xxx", + access_token: "xxx", + scope: "any", + }); + return createJSONResponse({}); + }, + port: 0, + }); + + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: "http://" + fakeFrogHost.hostname + ":" + fakeFrogHost.port, + artifactory_access_token: "XXXX", + package_managers: "{}", + }); +}); diff --git a/jfrog/main.tf b/jfrog/main.tf new file mode 100644 index 0000000..807bdf8 --- /dev/null +++ b/jfrog/main.tf @@ -0,0 +1,71 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + artifactory = { + source = "registry.terraform.io/jfrog/artifactory" + version = "~> 8.4.0" + } + } +} + +variable "jfrog_url" { + type = string + description = "JFrog instance URL. e.g. https://YYY.jfrog.io" +} + +variable "artifactory_access_token" { + type = string + description = "The admin-level access token to use for JFrog." +} + +# Configure the Artifactory provider +provider "artifactory" { + url = join("/", [var.jfrog_url, "artifactory"]) + access_token = var.artifactory_access_token +} +resource "artifactory_scoped_token" "me" { + # This is hacky, but on terraform plan the data source gives empty strings, + # which fails validation. + username = length(data.coder_workspace.me.owner_email) > 0 ? data.coder_workspace.me.owner_email : "plan" +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "package_managers" { + type = map(string) + description = < ~/.npmrc +email = ${ARTIFACTORY_USERNAME} +registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} +EOF + jf rt curl /api/npm/auth >> ~/.npmrc +fi + +# Configure the `pip` to use the Artifactory "python" repository. +if [ -z "${REPOSITORY_PYPI}" ]; then + echo "🤔 REPOSITORY_PYPI is not set, skipping pip configuration." +else + echo "🐍 Configuring pip..." + mkdir -p ~/.pip + cat << EOF > ~/.pip/pip.conf +[global] +index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple +EOF +fi + +# Set GOPROXY to use the Artifactory "go" repository. +if [ -z "${REPOSITORY_GO}" ]; then + echo "🤔 REPOSITORY_GO is not set, skipping go configuration." +else + echo "🐹 Configuring go..." + export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}" +fi +echo "🥳 Configuration complete!" \ No newline at end of file diff --git a/test.ts b/test.ts index b4c384f..747601d 100644 --- a/test.ts +++ b/test.ts @@ -153,7 +153,7 @@ export const testRequiredVariables = ( await runTerraformApply(dir, localVars); } catch (ex) { expect(ex.message).toContain( - `input variable \"${varName}\" is not set, and has no default`, + `input variable \"${varName}\" is not set`, ); return; } @@ -180,6 +180,7 @@ export const runTerraformApply = async ( "-input=false", "-auto-approve", "-state", + "-no-color", stateFile, ], { @@ -210,3 +211,12 @@ export const runTerraformInit = async (dir: string) => { throw new Error(text); } }; + +export const createJSONResponse = (obj: object, statusCode = 200): Response => { + return new Response(JSON.stringify(obj), { + headers: { + "Content-Type": "application/json", + }, + status: statusCode, + }) +} \ No newline at end of file