diff --git a/registry/coder/modules/jfrog-oauth/README.md b/registry/coder/modules/jfrog-oauth/README.md index 30727f089..5e53fa047 100644 --- a/registry/coder/modules/jfrog-oauth/README.md +++ b/registry/coder/modules/jfrog-oauth/README.md @@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.3" + version = "1.2.4" 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" @@ -57,7 +57,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.3" + version = "1.2.4" agent_id = coder_agent.main.id jfrog_url = "https://example.jfrog.io" username_field = "email" diff --git a/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl new file mode 100644 index 000000000..e89f78d97 --- /dev/null +++ b/registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl @@ -0,0 +1,400 @@ +# Test for jfrog-oauth module + +run "test_required_vars" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = {} + } + + # Mock external auth with valid access token for basic test + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } +} + +run "test_empty_access_token_fails" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = {} + } + + # Mock external auth with empty access token + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "" + } + } + + expect_failures = [ + resource.coder_script.jfrog + ] +} + +run "test_valid_access_token_succeeds" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = {} + } + + # Mock external auth with valid access token + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # Verify the script resource is created + assert { + condition = resource.coder_script.jfrog.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" + error_message = "coder_script display_name should be 'jfrog'" + } +} + +run "test_jfrog_url_validation" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "invalid-url" + package_managers = {} + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + expect_failures = [ + var.jfrog_url + ] +} + +run "test_username_field_validation" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + username_field = "invalid" + package_managers = {} + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + expect_failures = [ + var.username_field + ] +} + +run "test_with_npm_package_manager" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + npm = ["global", "@foo:foo", "@bar:bar"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + assert { + condition = resource.coder_script.jfrog.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\"") + 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") + 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") + error_message = "script should contain scoped npm registry for @bar" + } +} + +run "test_configure_code_server" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + configure_code_server = true + package_managers = {} + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # When configure_code_server is true, env vars should be created + assert { + condition = length(resource.coder_env.jfrog_ide_url) == 1 + error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true" + } + + assert { + condition = length(resource.coder_env.jfrog_ide_access_token) == 1 + error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true" + } +} + +run "test_go_proxy_env" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + go = ["foo", "bar", "baz"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # When go package manager is configured, GOPROXY env should be set + assert { + condition = length(resource.coder_env.goproxy) == 1 + error_message = "coder_env.goproxy should be created when go package manager is configured" + } + + # Verify GOPROXY contains all repos + assert { + condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo") + error_message = "GOPROXY should contain foo repo" + } + + assert { + condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar") + error_message = "GOPROXY should contain bar repo" + } + + assert { + condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz") + error_message = "GOPROXY should contain baz repo" + } + + # Verify script contains go configuration + assert { + condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"") + error_message = "script should contain jf goc command" + } +} + +run "test_pypi_package_manager" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + pypi = ["global", "foo", "bar"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # Verify pip configuration in script + assert { + condition = strcontains(resource.coder_script.jfrog.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") + error_message = "script should contain pip index-url configuration" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url") + error_message = "script should contain extra-index-url for additional repos" + } +} + +run "test_docker_package_manager" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # Verify docker registration commands in script + assert { + condition = strcontains(resource.coder_script.jfrog.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\"") + error_message = "script should contain register_docker for bar.jfrog.io" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"") + error_message = "script should contain register_docker for baz.jfrog.io" + } +} + +run "test_conda_package_manager" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + conda = ["conda-main", "conda-secondary", "conda-local"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # Verify conda configuration in script + assert { + condition = strcontains(resource.coder_script.jfrog.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") + error_message = "script should contain conda-main channel" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.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") + error_message = "script should contain conda-local channel" + } +} + +run "test_maven_package_manager" { + command = plan + + variables { + agent_id = "test-agent-id" + jfrog_url = "https://example.jfrog.io" + package_managers = { + maven = ["central", "snapshots", "local"] + } + } + + override_data { + target = data.coder_external_auth.jfrog + values = { + access_token = "valid-token-value" + } + } + + # Verify maven jf mvnc command + assert { + condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global") + error_message = "script should contain jf mvnc command" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.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\"") + error_message = "script should contain repo-resolve-snapshots for central" + } + + # Verify settings.xml content + assert { + condition = strcontains(resource.coder_script.jfrog.script, "") + error_message = "script should contain maven servers configuration" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "central") + error_message = "script should contain central server id" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "snapshots") + error_message = "script should contain snapshots server id" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "local") + error_message = "script should contain local server id" + } + + assert { + condition = strcontains(resource.coder_script.jfrog.script, "https://example.jfrog.io/artifactory/central") + error_message = "script should contain central repository URL" + } +} diff --git a/registry/coder/modules/jfrog-oauth/main.test.ts b/registry/coder/modules/jfrog-oauth/main.test.ts deleted file mode 100644 index a9c3a082a..000000000 --- a/registry/coder/modules/jfrog-oauth/main.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { - findResourceInstance, - runTerraformInit, - runTerraformApply, - testRequiredVariables, -} from "~test"; - -describe("jfrog-oauth", async () => { - type TestVariables = { - agent_id: string; - jfrog_url: string; - package_managers: string; - - username_field?: string; - jfrog_server_id?: string; - external_auth_id?: string; - configure_code_server?: boolean; - }; - - await runTerraformInit(import.meta.dir); - - const fakeFrogApi = "localhost:8081/artifactory/api"; - const fakeFrogUrl = "http://localhost:8081"; - const user = "default"; - - testRequiredVariables(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: "{}", - }); - - it("generates an npmrc with scoped repos", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - npm: ["global", "@foo:foo", "@bar:bar"], - }), - }); - const coderScript = findResourceInstance(state, "coder_script"); - const npmrcStanza = `cat << EOF > ~/.npmrc -email=${user}@example.com -registry=http://${fakeFrogApi}/npm/global -//${fakeFrogApi}/npm/global/:_authToken= -@foo:registry=http://${fakeFrogApi}/npm/foo -//${fakeFrogApi}/npm/foo/:_authToken= -@bar:registry=http://${fakeFrogApi}/npm/bar -//${fakeFrogApi}/npm/bar/:_authToken= - -EOF`; - expect(coderScript.script).toContain(npmrcStanza); - expect(coderScript.script).toContain( - 'jf npmc --global --repo-resolve "global"', - ); - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured npm', - ); - }); - - it("generates a pip config with extra-indexes", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - pypi: ["global", "foo", "bar"], - }), - }); - const coderScript = findResourceInstance(state, "coder_script"); - const pipStanza = `cat << EOF > ~/.pip/pip.conf -[global] -index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple -extra-index-url = - https://${user}:@${fakeFrogApi}/pypi/foo/simple - https://${user}:@${fakeFrogApi}/pypi/bar/simple - -EOF`; - expect(coderScript.script).toContain(pipStanza); - expect(coderScript.script).toContain( - 'jf pipc --global --repo-resolve "global"', - ); - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured pypi', - ); - }); - - it("registers multiple docker repos", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"], - }), - }); - const coderScript = findResourceInstance(state, "coder_script"); - const dockerStanza = ["foo", "bar", "baz"] - .map((r) => `register_docker "${r}.jfrog.io"`) - .join("\n"); - expect(coderScript.script).toContain(dockerStanza); - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured docker', - ); - }); - - it("sets goproxy with multiple repos", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - go: ["foo", "bar", "baz"], - }), - }); - const proxyEnv = findResourceInstance(state, "coder_env", "goproxy"); - const proxies = ["foo", "bar", "baz"] - .map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`) - .join(","); - expect(proxyEnv.value).toEqual(proxies); - - const coderScript = findResourceInstance(state, "coder_script"); - expect(coderScript.script).toContain( - 'jf goc --global --repo-resolve "foo"', - ); - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured go', - ); - }); - - it("generates a conda config with multiple repos", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - conda: ["conda-main", "conda-secondary", "conda-local"], - }), - }); - const coderScript = findResourceInstance(state, "coder_script"); - const condaStanza = `cat << EOF > ~/.condarc -channels: - - https://${user}:@${fakeFrogApi}/conda/conda-main - - https://${user}:@${fakeFrogApi}/conda/conda-secondary - - https://${user}:@${fakeFrogApi}/conda/conda-local - - defaults -ssl_verify: true - -EOF`; - expect(coderScript.script).toContain(condaStanza); - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured conda', - ); - }); - it("generates a maven settings.xml with multiple repos", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - package_managers: JSON.stringify({ - maven: ["central", "snapshots", "local"], - }), - }); - - const coderScript = findResourceInstance(state, "coder_script"); - - expect(coderScript.script).toContain("jf mvnc --global"); - expect(coderScript.script).toContain('--server-id-resolve="0"'); - expect(coderScript.script).toContain('--repo-resolve-releases "central"'); - expect(coderScript.script).toContain('--repo-resolve-snapshots "central"'); - expect(coderScript.script).toContain('--server-id-deploy="0"'); - expect(coderScript.script).toContain('--repo-deploy-releases "central"'); - expect(coderScript.script).toContain('--repo-deploy-snapshots "central"'); - - expect(coderScript.script).toContain(""); - expect(coderScript.script).toContain("central"); - expect(coderScript.script).toContain("snapshots"); - expect(coderScript.script).toContain("local"); - - expect(coderScript.script).toContain( - "http://localhost:8081/artifactory/central", - ); - expect(coderScript.script).toContain( - "http://localhost:8081/artifactory/snapshots", - ); - expect(coderScript.script).toContain( - "http://localhost:8081/artifactory/local", - ); - - expect(coderScript.script).toContain( - 'if [ -z "YES" ]; then\n not_configured maven', - ); - }); -}); diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 416ca3dc8..0bfb02bea 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -163,6 +163,13 @@ resource "coder_script" "jfrog" { } )) run_on_start = true + + lifecycle { + precondition { + condition = data.coder_external_auth.jfrog.access_token != "" + error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth." + } + } } resource "coder_env" "jfrog_ide_url" {