Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions registry/coder/modules/jfrog-oauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand All @@ -79,6 +79,23 @@ 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 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
}
```

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

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.
Expand All @@ -96,7 +113,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"
Expand Down Expand Up @@ -126,7 +143,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"
Expand Down
106 changes: 70 additions & 36 deletions registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,26 +17,28 @@ run "test_required_vars" {
}
}

run "test_empty_access_token_fails" {
run "test_empty_access_token_is_allowed_during_template_import" {
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
}

# 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 = {
access_token = ""
}
}

expect_failures = [
resource.coder_script.jfrog
]
assert {
condition = output.access_token == ""
error_message = "template import should allow an empty external auth token"
}
}

run "test_valid_access_token_succeeds" {
Expand All @@ -59,12 +60,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'"
}
}
Expand Down Expand Up @@ -131,23 +137,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"
}
}
Expand Down Expand Up @@ -223,7 +229,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"
}
}
Expand All @@ -248,17 +254,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"
}
}
Expand All @@ -283,17 +289,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"
}
}
Expand All @@ -318,22 +324,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"
}
}
Expand All @@ -358,43 +364,71 @@ 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, "<servers>")
condition = strcontains(resource.coder_script.jfrog[0].script, "<servers>")
error_message = "script should contain maven servers configuration"
}

assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
condition = strcontains(resource.coder_script.jfrog[0].script, "<id>central</id>")
error_message = "script should contain central server id"
}

assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
condition = strcontains(resource.coder_script.jfrog[0].script, "<id>snapshots</id>")
error_message = "script should contain snapshots server id"
}

assert {
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
condition = strcontains(resource.coder_script.jfrog[0].script, "<id>local</id>")
error_message = "script should contain local server id"
}

assert {
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
condition = strcontains(resource.coder_script.jfrog[0].script, "<url>https://example.jfrog.io/artifactory/central</url>")
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
}

override_data {
target = data.coder_external_auth.jfrog
values = {
access_token = "valid-token-value"
}
}

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"
}
}
Loading