From 7e3e2049bc6dfd8a3388778ae2b3104020a155ae Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:11:53 +0100 Subject: [PATCH 1/3] feat(jfrog): make workspace configuration optional --- registry/coder/modules/jfrog-oauth/README.md | 26 ++++++-- .../jfrog-oauth/jfrog-oauth.tftest.hcl | 45 +++++++++++-- registry/coder/modules/jfrog-oauth/main.tf | 26 +++++++- registry/coder/modules/jfrog-oauth/run.sh | 65 ++++++++++++------- registry/coder/modules/jfrog-token/README.md | 27 ++++++-- .../jfrog-token/jfrog-token.tftest.hcl | 58 +++++++++++++++++ .../coder/modules/jfrog-token/main.test.ts | 15 ++--- registry/coder/modules/jfrog-token/main.tf | 26 +++++++- registry/coder/modules/jfrog-token/run.sh | 63 +++++++++++------- 9 files changed, 276 insertions(+), 75 deletions(-) create mode 100644 registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl diff --git a/registry/coder/modules/jfrog-oauth/README.md b/registry/coder/modules/jfrog-oauth/README.md index 49668545e..3af07943a 100644 --- a/registry/coder/modules/jfrog-oauth/README.md +++ b/registry/coder/modules/jfrog-oauth/README.md @@ -16,7 +16,7 @@ Install the JFrog CLI (`jf`) and authenticate package managers (npm, Go, pip, Do module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.5" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://example.jfrog.io" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" @@ -70,7 +70,7 @@ Using the module requires two things: an application integration in Artifactory ## Offline and air-gapped environments -If `jf` is already on the `PATH` (for example, baked into your workspace image), the module detects it and skips the download. Otherwise, the startup script downloads it from `https://install-cli.jfrog.io` and installs it with `sudo`. In restricted or air-gapped environments, pre-install `jf` in your workspace image to avoid both the external download and the `sudo` step. +If `jf` is already on the `PATH` (for example, baked into your workspace image), set `install_jfrog_cli = false` to disable the download explicitly. Otherwise, the startup script downloads it from `https://install-cli.jfrog.io` and installs it with `sudo` when installation is enabled. In restricted or air-gapped environments, pre-install `jf` in your workspace image to avoid both the external download and the `sudo` step. ### External endpoints @@ -79,6 +79,24 @@ The module's startup script contacts: - `https://install-cli.jfrog.io`: only when the JFrog CLI is not already installed. - Your `jfrog_url` (for example, `https://example.jfrog.io`): to configure the package managers and exchange the OAuth token. +## Access-token-only mode + +If another Terraform resource only needs the OAuth `access_token` output, disable all workspace configuration. In this mode, the module still obtains the token through Coder external auth and keeps the existing `coder_script` resource for state compatibility, but does not run it, install or configure `jf`, or configure package managers. + +```tf +module "jfrog" { + source = "registry.coder.com/coder/jfrog-oauth/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false + configure_package_managers = false +} +``` + +Package manager configuration can be disabled independently with `configure_package_managers = false`, and `package_managers` can be omitted when no repositories are required. + ## Username Handling The module automatically extracts your JFrog username directly from the OAuth token's JWT payload. This preserves special characters like dots (`.`), hyphens (`-`), and accented characters that Coder normalizes in usernames. @@ -96,7 +114,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.5" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://example.jfrog.io" username_field = "email" @@ -126,7 +144,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.5" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://example.jfrog.io" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" diff --git a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl index e89f78d97..be2c517f1 100644 --- a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl +++ b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl @@ -4,9 +4,8 @@ run "test_required_vars" { command = plan variables { - agent_id = "test-agent-id" - jfrog_url = "https://example.jfrog.io" - package_managers = {} + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" } # Mock external auth with valid access token for basic test @@ -22,9 +21,11 @@ run "test_empty_access_token_fails" { command = plan variables { - agent_id = "test-agent-id" - jfrog_url = "https://example.jfrog.io" - package_managers = {} + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false + configure_package_managers = false } # Mock external auth with empty access token @@ -398,3 +399,35 @@ run "test_maven_package_manager" { error_message = "script should contain central repository URL" } } + +run "test_access_token_only" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false + configure_package_managers = false + package_managers = { + go = ["go"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + assert { + condition = !resource.coder_script.jfrog.run_on_start + error_message = "token-only mode should not run the workspace configuration script" + } + + assert { + condition = length(resource.coder_env.goproxy) == 0 + error_message = "token-only mode should not configure package managers" + } +} diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 0bfb02bea..09088a6eb 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -52,6 +52,24 @@ variable "configure_code_server" { default = false } +variable "install_jfrog_cli" { + description = "Whether to install the JFrog CLI when it is not already available on PATH." + type = bool + default = true +} + +variable "configure_jfrog_cli" { + description = "Whether to authenticate the JFrog CLI and configure shell completion." + type = bool + default = true +} + +variable "configure_package_managers" { + description = "Whether to configure the package managers listed in package_managers." + type = bool + default = true +} + variable "package_managers" { type = object({ npm = optional(list(string), []) @@ -73,6 +91,7 @@ variable "package_managers" { maven = ["YOUR_MAVEN_REPO_KEY", "ANOTHER_MAVEN_REPO_KEY"] } EOF + default = {} } locals { @@ -143,6 +162,9 @@ resource "coder_script" "jfrog" { script = templatefile("${path.module}/run.sh", merge( local.common_values, { + INSTALL_CLI = var.install_jfrog_cli + CONFIGURE_CLI = var.configure_jfrog_cli + CONFIGURE_PACKAGES = var.configure_package_managers CONFIGURE_CODE_SERVER = var.configure_code_server HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES" NPMRC = local.npmrc @@ -162,7 +184,7 @@ resource "coder_script" "jfrog" { REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "") } )) - run_on_start = true + run_on_start = var.install_jfrog_cli || var.configure_jfrog_cli || var.configure_package_managers || var.configure_code_server lifecycle { precondition { @@ -194,7 +216,7 @@ resource "coder_env" "jfrog_ide_store_connection" { } resource "coder_env" "goproxy" { - count = length(var.package_managers.go) == 0 ? 0 : 1 + count = var.configure_package_managers && length(var.package_managers.go) > 0 ? 1 : 0 agent_id = var.agent_id name = "GOPROXY" value = join(",", [ diff --git a/registry/coder/modules/jfrog-oauth/run.sh b/registry/coder/modules/jfrog-oauth/run.sh index 52f1e851e..360cb5e41 100644 --- a/registry/coder/modules/jfrog-oauth/run.sh +++ b/registry/coder/modules/jfrog-oauth/run.sh @@ -17,27 +17,38 @@ register_docker() { echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login "$repo" --username ${ARTIFACTORY_USERNAME} --password-stdin } -# check if JFrog CLI is already installed -if command -v jf > /dev/null 2>&1; then - echo "✅ JFrog CLI is already installed, skipping installation." +if [ "${INSTALL_CLI}" == "true" ]; then + if command -v jf > /dev/null 2>&1; then + echo "✅ JFrog CLI is already installed, skipping installation." + else + echo "📦 Installing JFrog CLI..." + curl -fL https://install-cli.jfrog.io | sudo sh + sudo chmod 755 /usr/local/bin/jf + fi else - echo "📦 Installing JFrog CLI..." - curl -fL https://install-cli.jfrog.io | sudo sh - sudo chmod 755 /usr/local/bin/jf + echo "🤔 Skipping JFrog CLI installation." fi -# The jf CLI checks $CI when determining whether to use interactive -# flows. -export CI=true -# Authenticate JFrog CLI with Artifactory. -echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}" -# Set the configured server as the default. -jf c use "${JFROG_SERVER_ID}" +if [ "${CONFIGURE_CLI}" == "true" ]; then + # The jf CLI checks $CI when determining whether to use interactive + # flows. + export CI=true + # Authenticate JFrog CLI with Artifactory. + echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}" + # Set the configured server as the default. + jf c use "${JFROG_SERVER_ID}" +else + echo "🤔 Skipping JFrog CLI configuration." +fi + +if [ "${CONFIGURE_PACKAGES}" != "true" ]; then + echo "🤔 Skipping package manager configuration." +fi # Configure npm to use the Artifactory "npm" repository. -if [ -z "${HAS_NPM}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_NPM}" ]; then not_configured npm -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc @@ -47,9 +58,9 @@ EOF fi # Configure the `pip` to use the Artifactory "python" repository. -if [ -z "${HAS_PYPI}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_PYPI}" ]; then not_configured pypi -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip @@ -60,18 +71,18 @@ EOF fi # Configure Artifactory "go" repository. -if [ -z "${HAS_GO}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_GO}" ]; then not_configured go -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" config_complete fi # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ -z "${HAS_DOCKER}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_DOCKER}" ]; then not_configured docker -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker @@ -82,9 +93,9 @@ else fi # Configure conda to use the Artifactory "conda" repository. -if [ -z "${HAS_CONDA}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_CONDA}" ]; then not_configured conda -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐍 Configuring conda..." # Create conda config directory if it doesn't exist mkdir -p ~/.conda @@ -95,9 +106,9 @@ EOF fi # Configure Maven to use the Artifactory "maven" repository. -if [ -z "${HAS_MAVEN}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_MAVEN}" ]; then not_configured maven -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "☕ Configuring maven..." jf mvnc --global \ --server-id-resolve="${JFROG_SERVER_ID}" \ @@ -133,6 +144,10 @@ else echo "🤔 Skipping JFrog extension installation. Set configure_code_server to true to install the JFrog extension." fi +if [ "${CONFIGURE_CLI}" != "true" ]; then + exit 0 +fi + # Configure the JFrog CLI completion echo "📦 Configuring JFrog CLI completion..." # Get the user's shell diff --git a/registry/coder/modules/jfrog-token/README.md b/registry/coder/modules/jfrog-token/README.md index 5252a8642..55197387a 100644 --- a/registry/coder/modules/jfrog-token/README.md +++ b/registry/coder/modules/jfrog-token/README.md @@ -13,7 +13,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti ```tf module "jfrog" { source = "registry.coder.com/coder/jfrog-token/coder" - version = "1.2.2" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token @@ -36,6 +36,25 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat ![JFrog](../../.images/jfrog.png) +## Token-only mode + +If another Terraform resource only needs the scoped `access_token` output, disable all workspace configuration. In this mode, the module keeps the existing `coder_script` resource for state compatibility but does not run it, install or configure `jf`, or configure package managers. + +```tf +module "jfrog" { + source = "registry.coder.com/coder/jfrog-token/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + jfrog_url = "https://XXXX.jfrog.io" + artifactory_access_token = var.artifactory_access_token + install_jfrog_cli = false + configure_jfrog_cli = false + configure_package_managers = false +} +``` + +To configure a pre-installed `jf` binary, set only `install_jfrog_cli = false` and leave `configure_jfrog_cli` enabled. Package manager configuration can be disabled independently with `configure_package_managers = false`, and `package_managers` can be omitted when no repositories are required. + ## Examples ### Configure npm, go, and pypi to use Artifactory local repositories @@ -43,7 +62,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat ```tf module "jfrog" { source = "registry.coder.com/coder/jfrog-token/coder" - version = "1.2.2" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://YYYY.jfrog.io" artifactory_access_token = var.artifactory_access_token # An admin access token @@ -83,7 +102,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio ```tf module "jfrog" { source = "registry.coder.com/coder/jfrog-token/coder" - version = "1.2.2" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token @@ -104,7 +123,7 @@ data "coder_workspace" "me" {} module "jfrog" { source = "registry.coder.com/coder/jfrog-token/coder" - version = "1.2.2" + version = "1.3.0" agent_id = coder_agent.main.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token diff --git a/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl b/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl new file mode 100644 index 000000000..5db3ce37a --- /dev/null +++ b/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl @@ -0,0 +1,58 @@ +run "test_defaults_preserve_workspace_configuration" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "http://127.0.0.1:1" + artifactory_access_token = "admin-token" + check_license = false + } + + override_data { + target = data.coder_workspace_owner.me + values = { + email = "coder@example.com" + name = "coder" + } + } + + assert { + condition = resource.coder_script.jfrog.run_on_start + error_message = "default settings should preserve the running workspace configuration script" + } +} + +run "test_access_token_only" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "http://127.0.0.1:1" + artifactory_access_token = "admin-token" + check_license = false + install_jfrog_cli = false + configure_jfrog_cli = false + configure_package_managers = false + package_managers = { + go = ["go"] + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + email = "coder@example.com" + name = "coder" + } + } + + assert { + condition = !resource.coder_script.jfrog.run_on_start + error_message = "token-only mode should not run the workspace configuration script" + } + + assert { + condition = length(resource.coder_env.goproxy) == 0 + error_message = "token-only mode should not configure package managers" + } +} diff --git a/registry/coder/modules/jfrog-token/main.test.ts b/registry/coder/modules/jfrog-token/main.test.ts index cabd7e80f..04ed1b90c 100644 --- a/registry/coder/modules/jfrog-token/main.test.ts +++ b/registry/coder/modules/jfrog-token/main.test.ts @@ -13,7 +13,7 @@ describe("jfrog-token", async () => { agent_id: string; jfrog_url: string; artifactory_access_token: string; - package_managers: string; + package_managers?: string; token_description?: string; check_license?: boolean; @@ -59,7 +59,6 @@ describe("jfrog-token", async () => { agent_id: "some-agent-id", jfrog_url: fakeFrogUrl, artifactory_access_token: "XXXX", - package_managers: "{}", }); it("generates an npmrc with scoped repos", async () => { @@ -87,7 +86,7 @@ EOF`; 'jf npmc --global --repo-resolve "global"', ); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured npm', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured npm', ); }); @@ -114,7 +113,7 @@ EOF`; 'jf pipc --global --repo-resolve "global"', ); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured pypi', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured pypi', ); }); @@ -133,7 +132,7 @@ EOF`; .join("\n"); expect(coderScript.script).toContain(dockerStanza); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured docker', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured docker', ); }); @@ -157,7 +156,7 @@ EOF`; 'jf goc --global --repo-resolve "foo"', ); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured go', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured go', ); }); @@ -182,7 +181,7 @@ ssl_verify: true EOF`; expect(coderScript.script).toContain(condaStanza); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured conda', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured conda', ); }); it("generates a maven settings.xml with multiple repos", async () => { @@ -221,7 +220,7 @@ EOF`; ); expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured maven', + 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured maven', ); }); }); diff --git a/registry/coder/modules/jfrog-token/main.tf b/registry/coder/modules/jfrog-token/main.tf index 4de91da5a..2e80fbcec 100644 --- a/registry/coder/modules/jfrog-token/main.tf +++ b/registry/coder/modules/jfrog-token/main.tf @@ -85,6 +85,24 @@ variable "configure_code_server" { default = false } +variable "install_jfrog_cli" { + description = "Whether to install the JFrog CLI when it is not already available on PATH." + type = bool + default = true +} + +variable "configure_jfrog_cli" { + description = "Whether to authenticate the JFrog CLI and configure shell completion." + type = bool + default = true +} + +variable "configure_package_managers" { + description = "Whether to configure the package managers listed in package_managers." + type = bool + default = true +} + variable "package_managers" { type = object({ npm = optional(list(string), []) @@ -106,6 +124,7 @@ variable "package_managers" { maven = ["YOUR_MAVEN_REPO_KEY", "ANOTHER_MAVEN_REPO_KEY"] } EOF + default = {} } locals { @@ -170,6 +189,9 @@ resource "coder_script" "jfrog" { script = templatefile("${path.module}/run.sh", merge( local.common_values, { + INSTALL_CLI = var.install_jfrog_cli + CONFIGURE_CLI = var.configure_jfrog_cli + CONFIGURE_PACKAGES = var.configure_package_managers CONFIGURE_CODE_SERVER = var.configure_code_server HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES" NPMRC = local.npmrc @@ -189,7 +211,7 @@ resource "coder_script" "jfrog" { REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "") } )) - run_on_start = true + run_on_start = var.install_jfrog_cli || var.configure_jfrog_cli || var.configure_package_managers || var.configure_code_server } resource "coder_env" "jfrog_ide_url" { @@ -214,7 +236,7 @@ resource "coder_env" "jfrog_ide_store_connection" { } resource "coder_env" "goproxy" { - count = length(var.package_managers.go) == 0 ? 0 : 1 + count = var.configure_package_managers && length(var.package_managers.go) > 0 ? 1 : 0 agent_id = var.agent_id name = "GOPROXY" value = join(",", [ diff --git a/registry/coder/modules/jfrog-token/run.sh b/registry/coder/modules/jfrog-token/run.sh index 182b37a2b..9e3526f77 100644 --- a/registry/coder/modules/jfrog-token/run.sh +++ b/registry/coder/modules/jfrog-token/run.sh @@ -17,26 +17,37 @@ register_docker() { echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login "$repo" --username ${ARTIFACTORY_USERNAME} --password-stdin } -# check if JFrog CLI is already installed -if command -v jf > /dev/null 2>&1; then - echo "✅ JFrog CLI is already installed, skipping installation." +if [ "${INSTALL_CLI}" == "true" ]; then + if command -v jf > /dev/null 2>&1; then + echo "✅ JFrog CLI is already installed, skipping installation." + else + echo "📦 Installing JFrog CLI..." + curl -fL https://install-cli.jfrog.io | sudo sh + sudo chmod 755 /usr/local/bin/jf + fi else - echo "📦 Installing JFrog CLI..." - curl -fL https://install-cli.jfrog.io | sudo sh - sudo chmod 755 /usr/local/bin/jf + echo "🤔 Skipping JFrog CLI installation." fi -# The jf CLI checks $CI when determining whether to use interactive flows. -export CI=true -# Authenticate JFrog CLI with Artifactory. -echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}" -# Set the configured server as the default. -jf c use "${JFROG_SERVER_ID}" +if [ "${CONFIGURE_CLI}" == "true" ]; then + # The jf CLI checks $CI when determining whether to use interactive flows. + export CI=true + # Authenticate JFrog CLI with Artifactory. + echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}" + # Set the configured server as the default. + jf c use "${JFROG_SERVER_ID}" +else + echo "🤔 Skipping JFrog CLI configuration." +fi + +if [ "${CONFIGURE_PACKAGES}" != "true" ]; then + echo "🤔 Skipping package manager configuration." +fi # Configure npm to use the Artifactory "npm" repository. -if [ -z "${HAS_NPM}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_NPM}" ]; then not_configured npm -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc @@ -46,9 +57,9 @@ EOF fi # Configure the `pip` to use the Artifactory "python" repository. -if [ -z "${HAS_PYPI}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_PYPI}" ]; then not_configured pypi -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip @@ -59,18 +70,18 @@ EOF fi # Configure Artifactory "go" repository. -if [ -z "${HAS_GO}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_GO}" ]; then not_configured go -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" config_complete fi # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ -z "${HAS_DOCKER}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_DOCKER}" ]; then not_configured docker -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker @@ -81,9 +92,9 @@ else fi # Configure conda to use the Artifactory "conda" repository. -if [ -z "${HAS_CONDA}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_CONDA}" ]; then not_configured conda -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "🐍 Configuring conda..." # Create conda config directory if it doesn't exist mkdir -p ~/.conda @@ -94,9 +105,9 @@ EOF fi # Configure Maven to use the Artifactory "maven" repository. -if [ -z "${HAS_MAVEN}" ]; then +if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_MAVEN}" ]; then not_configured maven -else +elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then echo "☕ Configuring maven..." jf mvnc --global \ --server-id-resolve="${JFROG_SERVER_ID}" \ @@ -132,6 +143,10 @@ else echo "🤔 Skipping JFrog extension installation. Set configure_code_server to true to install the JFrog extension." fi +if [ "${CONFIGURE_CLI}" != "true" ]; then + exit 0 +fi + # Configure the JFrog CLI completion echo "📦 Configuring JFrog CLI completion..." # Get the user's shell From 3035625be0e72df74ecb8714d1c6c1bed9648c5b Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:38:46 +0100 Subject: [PATCH 2/3] fix(jfrog): address optional configuration review --- registry/coder/modules/jfrog-oauth/README.md | 17 ++-- .../jfrog-oauth/jfrog-oauth.tftest.hcl | 82 +++++++++---------- registry/coder/modules/jfrog-oauth/main.tf | 42 ++++++---- registry/coder/modules/jfrog-oauth/run.sh | 35 ++++---- registry/coder/modules/jfrog-token/README.md | 19 ++--- .../jfrog-token/jfrog-token.tftest.hcl | 56 ++++++++++--- .../coder/modules/jfrog-token/main.test.ts | 43 ++++++++-- registry/coder/modules/jfrog-token/main.tf | 28 +++++-- registry/coder/modules/jfrog-token/run.sh | 35 ++++---- 9 files changed, 219 insertions(+), 138 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/README.md b/registry/coder/modules/jfrog-oauth/README.md index 3af07943a..8f5a7f6ff 100644 --- a/registry/coder/modules/jfrog-oauth/README.md +++ b/registry/coder/modules/jfrog-oauth/README.md @@ -81,21 +81,20 @@ The module's startup script contacts: ## Access-token-only mode -If another Terraform resource only needs the OAuth `access_token` output, disable all workspace configuration. In this mode, the module still obtains the token through Coder external auth and keeps the existing `coder_script` resource for state compatibility, but does not run it, install or configure `jf`, or configure package managers. +If another Terraform resource only needs the OAuth `access_token` output, disable JFrog CLI installation and configuration, and omit `package_managers`. In this mode, the module still obtains the token through Coder external auth but does not create a workspace configuration script, install or configure `jf`, or configure package managers. ```tf module "jfrog" { - source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.3.0" - agent_id = coder_agent.main.id - jfrog_url = "https://example.jfrog.io" - install_jfrog_cli = false - configure_jfrog_cli = false - configure_package_managers = false + source = "registry.coder.com/coder/jfrog-oauth/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false } ``` -Package manager configuration can be disabled independently with `configure_package_managers = false`, and `package_managers` can be omitted when no repositories are required. +The module fails with a clear error if `jf` is required but unavailable. Package manager configuration is disabled by omitting `package_managers` or leaving it empty. ## Username Handling diff --git a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl index be2c517f1..676156d96 100644 --- a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl +++ b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl @@ -21,11 +21,10 @@ run "test_empty_access_token_fails" { command = plan variables { - agent_id = "test-agent-id" - jfrog_url = "https://example.jfrog.io" - install_jfrog_cli = false - configure_jfrog_cli = false - configure_package_managers = false + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false } # Mock external auth with empty access token @@ -37,7 +36,7 @@ run "test_empty_access_token_fails" { } expect_failures = [ - resource.coder_script.jfrog + data.coder_external_auth.jfrog ] } @@ -60,12 +59,17 @@ run "test_valid_access_token_succeeds" { # Verify the script resource is created assert { - condition = resource.coder_script.jfrog.agent_id == "test-agent-id" + condition = length(resource.coder_script.jfrog) == 1 + error_message = "default settings should create one coder_script" + } + + assert { + condition = resource.coder_script.jfrog[0].agent_id == "test-agent-id" error_message = "coder_script agent_id should match the input variable" } assert { - condition = resource.coder_script.jfrog.display_name == "jfrog" + condition = resource.coder_script.jfrog[0].display_name == "jfrog" error_message = "coder_script display_name should be 'jfrog'" } } @@ -132,23 +136,23 @@ run "test_with_npm_package_manager" { } assert { - condition = resource.coder_script.jfrog.run_on_start == true + condition = resource.coder_script.jfrog[0].run_on_start == true error_message = "coder_script should run on start" } # Verify npm configuration is in script assert { - condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "jf npmc --global --repo-resolve \"global\"") error_message = "script should contain jf npmc command for npm" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo") + condition = strcontains(resource.coder_script.jfrog[0].script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo") error_message = "script should contain scoped npm registry for @foo" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar") + condition = strcontains(resource.coder_script.jfrog[0].script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar") error_message = "script should contain scoped npm registry for @bar" } } @@ -224,7 +228,7 @@ run "test_go_proxy_env" { # Verify script contains go configuration assert { - condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "jf goc --global --repo-resolve \"foo\"") error_message = "script should contain jf goc command" } } @@ -249,17 +253,17 @@ run "test_pypi_package_manager" { # Verify pip configuration in script assert { - condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "jf pipc --global --repo-resolve \"global\"") error_message = "script should contain jf pipc command" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple") + condition = strcontains(resource.coder_script.jfrog[0].script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple") error_message = "script should contain pip index-url configuration" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url") + condition = strcontains(resource.coder_script.jfrog[0].script, "extra-index-url") error_message = "script should contain extra-index-url for additional repos" } } @@ -284,17 +288,17 @@ run "test_docker_package_manager" { # Verify docker registration commands in script assert { - condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "register_docker \"foo.jfrog.io\"") error_message = "script should contain register_docker for foo.jfrog.io" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "register_docker \"bar.jfrog.io\"") error_message = "script should contain register_docker for bar.jfrog.io" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "register_docker \"baz.jfrog.io\"") error_message = "script should contain register_docker for baz.jfrog.io" } } @@ -319,22 +323,22 @@ run "test_conda_package_manager" { # Verify conda configuration in script assert { - condition = strcontains(resource.coder_script.jfrog.script, "channels:") + condition = strcontains(resource.coder_script.jfrog[0].script, "channels:") error_message = "script should contain conda channels configuration" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main") + condition = strcontains(resource.coder_script.jfrog[0].script, "example.jfrog.io/artifactory/api/conda/conda-main") error_message = "script should contain conda-main channel" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary") + condition = strcontains(resource.coder_script.jfrog[0].script, "example.jfrog.io/artifactory/api/conda/conda-secondary") error_message = "script should contain conda-secondary channel" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local") + condition = strcontains(resource.coder_script.jfrog[0].script, "example.jfrog.io/artifactory/api/conda/conda-local") error_message = "script should contain conda-local channel" } } @@ -359,43 +363,43 @@ run "test_maven_package_manager" { # Verify maven jf mvnc command assert { - condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global") + condition = strcontains(resource.coder_script.jfrog[0].script, "jf mvnc --global") error_message = "script should contain jf mvnc command" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "--repo-resolve-releases \"central\"") error_message = "script should contain repo-resolve-releases for central" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"") + condition = strcontains(resource.coder_script.jfrog[0].script, "--repo-resolve-snapshots \"central\"") error_message = "script should contain repo-resolve-snapshots for central" } # Verify settings.xml content assert { - condition = strcontains(resource.coder_script.jfrog.script, "") + condition = strcontains(resource.coder_script.jfrog[0].script, "") error_message = "script should contain maven servers configuration" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "central") + condition = strcontains(resource.coder_script.jfrog[0].script, "central") error_message = "script should contain central server id" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "snapshots") + condition = strcontains(resource.coder_script.jfrog[0].script, "snapshots") error_message = "script should contain snapshots server id" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "local") + condition = strcontains(resource.coder_script.jfrog[0].script, "local") error_message = "script should contain local server id" } assert { - condition = strcontains(resource.coder_script.jfrog.script, "https://example.jfrog.io/artifactory/central") + condition = strcontains(resource.coder_script.jfrog[0].script, "https://example.jfrog.io/artifactory/central") error_message = "script should contain central repository URL" } } @@ -404,14 +408,10 @@ run "test_access_token_only" { command = plan variables { - agent_id = "test-agent-id" - jfrog_url = "https://example.jfrog.io" - install_jfrog_cli = false - configure_jfrog_cli = false - configure_package_managers = false - package_managers = { - go = ["go"] - } + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + install_jfrog_cli = false + configure_jfrog_cli = false } override_data { @@ -422,8 +422,8 @@ run "test_access_token_only" { } assert { - condition = !resource.coder_script.jfrog.run_on_start - error_message = "token-only mode should not run the workspace configuration script" + condition = length(resource.coder_script.jfrog) == 0 + error_message = "token-only mode should not create the workspace configuration script" } assert { diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 09088a6eb..14d5e5178 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -64,12 +64,6 @@ variable "configure_jfrog_cli" { default = true } -variable "configure_package_managers" { - description = "Whether to configure the package managers listed in package_managers." - type = bool - default = true -} - variable "package_managers" { type = object({ npm = optional(list(string), []) @@ -117,6 +111,21 @@ locals { var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name ) jfrog_host = split("://", var.jfrog_url)[1] + has_package_managers = anytrue([ + length(var.package_managers.npm) > 0, + length(var.package_managers.go) > 0, + length(var.package_managers.pypi) > 0, + length(var.package_managers.docker) > 0, + length(var.package_managers.conda) > 0, + length(var.package_managers.maven) > 0, + ]) + requires_jfrog_cli = var.configure_jfrog_cli || anytrue([ + length(var.package_managers.npm) > 0, + length(var.package_managers.go) > 0, + length(var.package_managers.pypi) > 0, + length(var.package_managers.maven) > 0, + ]) + configure_workspace = var.install_jfrog_cli || var.configure_jfrog_cli || local.has_package_managers || var.configure_code_server common_values = { JFROG_URL = var.jfrog_url JFROG_HOST = local.jfrog_host @@ -153,9 +162,17 @@ data "coder_workspace_owner" "me" {} data "coder_external_auth" "jfrog" { id = var.external_auth_id + + lifecycle { + postcondition { + condition = self.access_token != "" + error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth." + } + } } resource "coder_script" "jfrog" { + count = local.configure_workspace ? 1 : 0 agent_id = var.agent_id display_name = "jfrog" icon = "/icon/jfrog.svg" @@ -164,7 +181,7 @@ resource "coder_script" "jfrog" { { INSTALL_CLI = var.install_jfrog_cli CONFIGURE_CLI = var.configure_jfrog_cli - CONFIGURE_PACKAGES = var.configure_package_managers + REQUIRE_CLI = local.requires_jfrog_cli CONFIGURE_CODE_SERVER = var.configure_code_server HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES" NPMRC = local.npmrc @@ -184,14 +201,7 @@ resource "coder_script" "jfrog" { REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "") } )) - run_on_start = var.install_jfrog_cli || var.configure_jfrog_cli || var.configure_package_managers || var.configure_code_server - - lifecycle { - precondition { - condition = data.coder_external_auth.jfrog.access_token != "" - error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth." - } - } + run_on_start = true } resource "coder_env" "jfrog_ide_url" { @@ -216,7 +226,7 @@ resource "coder_env" "jfrog_ide_store_connection" { } resource "coder_env" "goproxy" { - count = var.configure_package_managers && length(var.package_managers.go) > 0 ? 1 : 0 + count = length(var.package_managers.go) > 0 ? 1 : 0 agent_id = var.agent_id name = "GOPROXY" value = join(",", [ diff --git a/registry/coder/modules/jfrog-oauth/run.sh b/registry/coder/modules/jfrog-oauth/run.sh index 360cb5e41..76d8d5df9 100644 --- a/registry/coder/modules/jfrog-oauth/run.sh +++ b/registry/coder/modules/jfrog-oauth/run.sh @@ -29,6 +29,11 @@ else echo "🤔 Skipping JFrog CLI installation." fi +if [ "${REQUIRE_CLI}" == "true" ] && ! command -v jf > /dev/null 2>&1; then + echo "❌ JFrog CLI is required but was not found on PATH. Install it or enable install_jfrog_cli." >&2 + exit 1 +fi + if [ "${CONFIGURE_CLI}" == "true" ]; then # The jf CLI checks $CI when determining whether to use interactive # flows. @@ -41,14 +46,10 @@ else echo "🤔 Skipping JFrog CLI configuration." fi -if [ "${CONFIGURE_PACKAGES}" != "true" ]; then - echo "🤔 Skipping package manager configuration." -fi - # Configure npm to use the Artifactory "npm" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_NPM}" ]; then +if [ -z "${HAS_NPM}" ]; then not_configured npm -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc @@ -58,9 +59,9 @@ EOF fi # Configure the `pip` to use the Artifactory "python" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_PYPI}" ]; then +if [ -z "${HAS_PYPI}" ]; then not_configured pypi -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip @@ -71,18 +72,18 @@ EOF fi # Configure Artifactory "go" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_GO}" ]; then +if [ -z "${HAS_GO}" ]; then not_configured go -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" config_complete fi # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_DOCKER}" ]; then +if [ -z "${HAS_DOCKER}" ]; then not_configured docker -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker @@ -93,9 +94,9 @@ elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then fi # Configure conda to use the Artifactory "conda" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_CONDA}" ]; then +if [ -z "${HAS_CONDA}" ]; then not_configured conda -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐍 Configuring conda..." # Create conda config directory if it doesn't exist mkdir -p ~/.conda @@ -106,9 +107,9 @@ EOF fi # Configure Maven to use the Artifactory "maven" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_MAVEN}" ]; then +if [ -z "${HAS_MAVEN}" ]; then not_configured maven -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "☕ Configuring maven..." jf mvnc --global \ --server-id-resolve="${JFROG_SERVER_ID}" \ @@ -127,8 +128,8 @@ fi # Install the JFrog vscode extension for code-server. if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then + counter=0 while ! [ -x /tmp/code-server/bin/code-server ]; do - counter=0 if [ $counter -eq 60 ]; then echo "Timed out waiting for /tmp/code-server/bin/code-server to be installed." exit 1 diff --git a/registry/coder/modules/jfrog-token/README.md b/registry/coder/modules/jfrog-token/README.md index 55197387a..1bc3ce102 100644 --- a/registry/coder/modules/jfrog-token/README.md +++ b/registry/coder/modules/jfrog-token/README.md @@ -38,22 +38,21 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat ## Token-only mode -If another Terraform resource only needs the scoped `access_token` output, disable all workspace configuration. In this mode, the module keeps the existing `coder_script` resource for state compatibility but does not run it, install or configure `jf`, or configure package managers. +If another Terraform resource only needs the scoped `access_token` output, disable JFrog CLI installation and configuration, and omit `package_managers`. In this mode, the module does not create a workspace configuration script, install or configure `jf`, or configure package managers. ```tf module "jfrog" { - source = "registry.coder.com/coder/jfrog-token/coder" - version = "1.3.0" - agent_id = coder_agent.main.id - jfrog_url = "https://XXXX.jfrog.io" - artifactory_access_token = var.artifactory_access_token - install_jfrog_cli = false - configure_jfrog_cli = false - configure_package_managers = false + source = "registry.coder.com/coder/jfrog-token/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + jfrog_url = "https://XXXX.jfrog.io" + artifactory_access_token = var.artifactory_access_token + install_jfrog_cli = false + configure_jfrog_cli = false } ``` -To configure a pre-installed `jf` binary, set only `install_jfrog_cli = false` and leave `configure_jfrog_cli` enabled. Package manager configuration can be disabled independently with `configure_package_managers = false`, and `package_managers` can be omitted when no repositories are required. +To configure a pre-installed `jf` binary, set only `install_jfrog_cli = false` and leave `configure_jfrog_cli` enabled. The module fails with a clear error if `jf` is required but unavailable. Package manager configuration is disabled by omitting `package_managers` or leaving it empty. ## Examples diff --git a/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl b/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl index 5db3ce37a..93bc8ad46 100644 --- a/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl +++ b/registry/coder/modules/jfrog-token/jfrog-token.tftest.hcl @@ -17,8 +17,8 @@ run "test_defaults_preserve_workspace_configuration" { } assert { - condition = resource.coder_script.jfrog.run_on_start - error_message = "default settings should preserve the running workspace configuration script" + condition = length(resource.coder_script.jfrog) == 1 && resource.coder_script.jfrog[0].run_on_start + error_message = "default settings should create the workspace configuration script" } } @@ -26,13 +26,43 @@ run "test_access_token_only" { command = plan variables { - agent_id = "test-agent-id" - jfrog_url = "http://127.0.0.1:1" - artifactory_access_token = "admin-token" - check_license = false - install_jfrog_cli = false - configure_jfrog_cli = false - configure_package_managers = false + agent_id = "test-agent-id" + jfrog_url = "http://127.0.0.1:1" + artifactory_access_token = "admin-token" + check_license = false + install_jfrog_cli = false + configure_jfrog_cli = false + } + + override_data { + target = data.coder_workspace_owner.me + values = { + email = "coder@example.com" + name = "coder" + } + } + + assert { + condition = length(resource.coder_script.jfrog) == 0 + error_message = "token-only mode should not create the workspace configuration script" + } + + assert { + condition = length(resource.coder_env.goproxy) == 0 + error_message = "token-only mode should not configure package managers" + } +} + +run "test_package_manager_only" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "http://127.0.0.1:1" + artifactory_access_token = "admin-token" + check_license = false + install_jfrog_cli = false + configure_jfrog_cli = false package_managers = { go = ["go"] } @@ -47,12 +77,12 @@ run "test_access_token_only" { } assert { - condition = !resource.coder_script.jfrog.run_on_start - error_message = "token-only mode should not run the workspace configuration script" + condition = length(resource.coder_script.jfrog) == 1 + error_message = "package manager configuration should create the workspace configuration script" } assert { - condition = length(resource.coder_env.goproxy) == 0 - error_message = "token-only mode should not configure package managers" + condition = length(resource.coder_env.goproxy) == 1 + error_message = "go package manager configuration should set GOPROXY" } } diff --git a/registry/coder/modules/jfrog-token/main.test.ts b/registry/coder/modules/jfrog-token/main.test.ts index 04ed1b90c..67798d7fd 100644 --- a/registry/coder/modules/jfrog-token/main.test.ts +++ b/registry/coder/modules/jfrog-token/main.test.ts @@ -23,6 +23,8 @@ describe("jfrog-token", async () => { username?: string; jfrog_server_id?: string; configure_code_server?: boolean; + install_jfrog_cli?: boolean; + configure_jfrog_cli?: boolean; }; await runTerraformInit(import.meta.dir); @@ -86,7 +88,7 @@ EOF`; 'jf npmc --global --repo-resolve "global"', ); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured npm', + 'if [ -z "YES" ]; then\n not_configured npm', ); }); @@ -113,7 +115,7 @@ EOF`; 'jf pipc --global --repo-resolve "global"', ); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured pypi', + 'if [ -z "YES" ]; then\n not_configured pypi', ); }); @@ -122,6 +124,8 @@ EOF`; agent_id: "some-agent-id", jfrog_url: fakeFrogUrl, artifactory_access_token: "XXXX", + install_jfrog_cli: false, + configure_jfrog_cli: false, package_managers: JSON.stringify({ docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"], }), @@ -132,7 +136,10 @@ EOF`; .join("\n"); expect(coderScript.script).toContain(dockerStanza); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured docker', + 'if [ -z "YES" ]; then\n not_configured docker', + ); + expect(coderScript.script).toContain( + 'if [ "false" == "true" ] && ! command -v jf', ); }); @@ -156,7 +163,7 @@ EOF`; 'jf goc --global --repo-resolve "foo"', ); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured go', + 'if [ -z "YES" ]; then\n not_configured go', ); }); @@ -181,7 +188,7 @@ ssl_verify: true EOF`; expect(coderScript.script).toContain(condaStanza); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured conda', + 'if [ -z "YES" ]; then\n not_configured conda', ); }); it("generates a maven settings.xml with multiple repos", async () => { @@ -220,7 +227,31 @@ EOF`; ); expect(coderScript.script).toContain( - 'if [ "true" == "true" ] && [ -z "YES" ]; then\n not_configured maven', + 'if [ -z "YES" ]; then\n not_configured maven', + ); + }); + + it("renders a clear error when a required preinstalled CLI is missing", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: fakeFrogUrl, + artifactory_access_token: "XXXX", + install_jfrog_cli: false, + configure_jfrog_cli: true, + }); + const coderScript = findResourceInstance(state, "coder_script"); + + expect(coderScript.script).toContain( + 'if [ "true" == "true" ] && ! command -v jf', + ); + expect(coderScript.script).toContain( + "JFrog CLI is required but was not found on PATH", + ); + expect(coderScript.script).toContain( + "counter=0\n while ! [ -x /tmp/code-server/bin/code-server ]; do", + ); + expect(coderScript.script).not.toContain( + "while ! [ -x /tmp/code-server/bin/code-server ]; do\n counter=0", ); }); }); diff --git a/registry/coder/modules/jfrog-token/main.tf b/registry/coder/modules/jfrog-token/main.tf index 2e80fbcec..1185e8862 100644 --- a/registry/coder/modules/jfrog-token/main.tf +++ b/registry/coder/modules/jfrog-token/main.tf @@ -97,12 +97,6 @@ variable "configure_jfrog_cli" { default = true } -variable "configure_package_managers" { - description = "Whether to configure the package managers listed in package_managers." - type = bool - default = true -} - variable "package_managers" { type = object({ npm = optional(list(string), []) @@ -131,6 +125,21 @@ locals { # The username to use for artifactory username = coalesce(var.username, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name) jfrog_host = split("://", var.jfrog_url)[1] + has_package_managers = anytrue([ + length(var.package_managers.npm) > 0, + length(var.package_managers.go) > 0, + length(var.package_managers.pypi) > 0, + length(var.package_managers.docker) > 0, + length(var.package_managers.conda) > 0, + length(var.package_managers.maven) > 0, + ]) + requires_jfrog_cli = var.configure_jfrog_cli || anytrue([ + length(var.package_managers.npm) > 0, + length(var.package_managers.go) > 0, + length(var.package_managers.pypi) > 0, + length(var.package_managers.maven) > 0, + ]) + configure_workspace = var.install_jfrog_cli || var.configure_jfrog_cli || local.has_package_managers || var.configure_code_server common_values = { JFROG_URL = var.jfrog_url JFROG_HOST = local.jfrog_host @@ -183,6 +192,7 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} resource "coder_script" "jfrog" { + count = local.configure_workspace ? 1 : 0 agent_id = var.agent_id display_name = "jfrog" icon = "/icon/jfrog.svg" @@ -191,7 +201,7 @@ resource "coder_script" "jfrog" { { INSTALL_CLI = var.install_jfrog_cli CONFIGURE_CLI = var.configure_jfrog_cli - CONFIGURE_PACKAGES = var.configure_package_managers + REQUIRE_CLI = local.requires_jfrog_cli CONFIGURE_CODE_SERVER = var.configure_code_server HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES" NPMRC = local.npmrc @@ -211,7 +221,7 @@ resource "coder_script" "jfrog" { REPOSITORY_MAVEN = try(element(var.package_managers.maven, 0), "") } )) - run_on_start = var.install_jfrog_cli || var.configure_jfrog_cli || var.configure_package_managers || var.configure_code_server + run_on_start = true } resource "coder_env" "jfrog_ide_url" { @@ -236,7 +246,7 @@ resource "coder_env" "jfrog_ide_store_connection" { } resource "coder_env" "goproxy" { - count = var.configure_package_managers && length(var.package_managers.go) > 0 ? 1 : 0 + count = length(var.package_managers.go) > 0 ? 1 : 0 agent_id = var.agent_id name = "GOPROXY" value = join(",", [ diff --git a/registry/coder/modules/jfrog-token/run.sh b/registry/coder/modules/jfrog-token/run.sh index 9e3526f77..1ced562dd 100644 --- a/registry/coder/modules/jfrog-token/run.sh +++ b/registry/coder/modules/jfrog-token/run.sh @@ -29,6 +29,11 @@ else echo "🤔 Skipping JFrog CLI installation." fi +if [ "${REQUIRE_CLI}" == "true" ] && ! command -v jf > /dev/null 2>&1; then + echo "❌ JFrog CLI is required but was not found on PATH. Install it or enable install_jfrog_cli." >&2 + exit 1 +fi + if [ "${CONFIGURE_CLI}" == "true" ]; then # The jf CLI checks $CI when determining whether to use interactive flows. export CI=true @@ -40,14 +45,10 @@ else echo "🤔 Skipping JFrog CLI configuration." fi -if [ "${CONFIGURE_PACKAGES}" != "true" ]; then - echo "🤔 Skipping package manager configuration." -fi - # Configure npm to use the Artifactory "npm" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_NPM}" ]; then +if [ -z "${HAS_NPM}" ]; then not_configured npm -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc @@ -57,9 +58,9 @@ EOF fi # Configure the `pip` to use the Artifactory "python" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_PYPI}" ]; then +if [ -z "${HAS_PYPI}" ]; then not_configured pypi -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip @@ -70,18 +71,18 @@ EOF fi # Configure Artifactory "go" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_GO}" ]; then +if [ -z "${HAS_GO}" ]; then not_configured go -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" config_complete fi # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_DOCKER}" ]; then +if [ -z "${HAS_DOCKER}" ]; then not_configured docker -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker @@ -92,9 +93,9 @@ elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then fi # Configure conda to use the Artifactory "conda" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_CONDA}" ]; then +if [ -z "${HAS_CONDA}" ]; then not_configured conda -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "🐍 Configuring conda..." # Create conda config directory if it doesn't exist mkdir -p ~/.conda @@ -105,9 +106,9 @@ EOF fi # Configure Maven to use the Artifactory "maven" repository. -if [ "${CONFIGURE_PACKAGES}" == "true" ] && [ -z "${HAS_MAVEN}" ]; then +if [ -z "${HAS_MAVEN}" ]; then not_configured maven -elif [ "${CONFIGURE_PACKAGES}" == "true" ]; then +else echo "☕ Configuring maven..." jf mvnc --global \ --server-id-resolve="${JFROG_SERVER_ID}" \ @@ -126,8 +127,8 @@ fi # Install the JFrog vscode extension for code-server. if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then + counter=0 while ! [ -x /tmp/code-server/bin/code-server ]; do - counter=0 if [ $counter -eq 60 ]; then echo "Timed out waiting for /tmp/code-server/bin/code-server to be installed." exit 1 From 8e38a37b501c34647304a353df8c43f1d7f1404c Mon Sep 17 00:00:00 2001 From: eddymarc <132223353+Edd88-pixel@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:36:22 +0100 Subject: [PATCH 3/3] fix(jfrog): allow OAuth template import --- .../coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl | 11 ++++++----- registry/coder/modules/jfrog-oauth/main.tf | 7 ------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl index 676156d96..726929e4c 100644 --- a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl +++ b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl @@ -17,7 +17,7 @@ run "test_required_vars" { } } -run "test_empty_access_token_fails" { +run "test_empty_access_token_is_allowed_during_template_import" { command = plan variables { @@ -27,7 +27,7 @@ run "test_empty_access_token_fails" { configure_jfrog_cli = false } - # Mock external auth with empty access token + # Coder returns an empty token while importing a template, before a user can authenticate. override_data { target = data.coder_external_auth.jfrog values = { @@ -35,9 +35,10 @@ run "test_empty_access_token_fails" { } } - expect_failures = [ - data.coder_external_auth.jfrog - ] + assert { + condition = output.access_token == "" + error_message = "template import should allow an empty external auth token" + } } run "test_valid_access_token_succeeds" { diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 14d5e5178..4702ca045 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -162,13 +162,6 @@ data "coder_workspace_owner" "me" {} data "coder_external_auth" "jfrog" { id = var.external_auth_id - - lifecycle { - postcondition { - condition = self.access_token != "" - error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth." - } - } } resource "coder_script" "jfrog" {