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
36 changes: 31 additions & 5 deletions registry/coder/modules/jfrog-oauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ tags: [integration, jfrog, helper]

# JFrog

Install the JFrog CLI (`jf`) and authenticate package managers (npm, Go, pip, Docker, Conda, and Maven) with Artifactory using OAuth, configured via the Coder [`external-auth`](https://coder.com/docs/admin/external-auth) feature. Each user authenticates through an OAuth flow and receives a user-scoped access token, so no API keys or passwords are stored in the template or the workspace.
Install the JFrog CLI (`jf`) and authenticate package managers (npm, pnpm, Go, pip, Docker, Conda, and Maven) with Artifactory using OAuth, configured via the Coder [`external-auth`](https://coder.com/docs/admin/external-auth) feature. Each user authenticates through an OAuth flow and receives a user-scoped access token, so no API keys or passwords are stored in the template or the workspace.

![JFrog OAuth](../../.images/jfrog-oauth.png)

```tf
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"

package_managers = {
npm = ["npm", "@scoped:npm-scoped"]
pnpm = ["npm", "@scoped:npm-scoped"]
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
Expand All @@ -34,7 +35,9 @@ module "jfrog" {
```

> Note
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
> This module does not install `npm`, `pnpm`, `go`, `pip`, etc but only configures them. You need to handle the installation of these tools yourself.
>
> `jf pnpm` requires Node.js 20 or newer and pnpm 10.x or 11.x. Use JFrog CLI 2.98.0 or newer for pnpm 10.x and 2.116.0 or newer for pnpm 11.x.

## Prerequisites

Expand Down Expand Up @@ -90,13 +93,36 @@ The module automatically extracts your JFrog username directly from the OAuth to

## Examples

### Configure pnpm to use an Artifactory npm repository

```tf
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.3.0"
agent_id = coder_agent.main.id
jfrog_url = "https://example.jfrog.io"
username_field = "username"

package_managers = {
pnpm = ["npm-local"]
}
}
```

pnpm and npm share `~/.npmrc`, so their repository lists must match when both are configured. After pnpm is installed in the workspace, run:

```shell
jf pnpm install
```

Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username.

```tf
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 +152,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
88 changes: 88 additions & 0 deletions registry/coder/modules/jfrog-oauth/jfrog-oauth.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,94 @@ run "test_with_npm_package_manager" {
}
}

run "test_with_pnpm_package_manager" {
command = plan

variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
pnpm = ["global", "@foo:foo"]
}
}

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

assert {
condition = strcontains(resource.coder_script.jfrog.script, "jf pnpmc --global --repo-resolve \"global\"")
error_message = "script should contain jf pnpmc command for pnpm"
}

assert {
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
error_message = "script should contain scoped pnpm registry for @foo"
}
}

run "test_with_npm_and_pnpm_package_managers" {
command = plan

variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
npm = ["global", "@foo:foo"]
pnpm = ["global", "@foo:foo"]
}
}

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

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, "jf pnpmc --global --repo-resolve \"global\"")
error_message = "script should contain jf pnpmc command for pnpm"
}

assert {
condition = length(regexall("cat << EOF > ~/.npmrc", resource.coder_script.jfrog.script)) == 1
error_message = "script should write the shared npmrc exactly once"
}
}

run "test_mismatched_npm_pnpm_repositories" {
command = plan

variables {
agent_id = "test-agent-id"
jfrog_url = "https://example.jfrog.io"
package_managers = {
npm = ["npm-virtual"]
pnpm = ["pnpm-virtual"]
}
}

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

expect_failures = [
var.package_managers,
]
}

run "test_configure_code_server" {
command = plan

Expand Down
16 changes: 15 additions & 1 deletion registry/coder/modules/jfrog-oauth/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ variable "configure_code_server" {
variable "package_managers" {
type = object({
npm = optional(list(string), [])
pnpm = optional(list(string), [])
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
Expand All @@ -66,13 +67,23 @@ variable "package_managers" {
For example:
{
npm = ["GLOBAL_NPM_REPO_KEY", "@SCOPED:NPM_REPO_KEY"]
pnpm = ["GLOBAL_NPM_REPO_KEY", "@SCOPED:NPM_REPO_KEY"]
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
maven = ["YOUR_MAVEN_REPO_KEY", "ANOTHER_MAVEN_REPO_KEY"]
}
EOF

validation {
condition = (
length(var.package_managers.npm) == 0 ||
length(var.package_managers.pnpm) == 0 ||
var.package_managers.npm == var.package_managers.pnpm
)
error_message = "package_managers.npm and package_managers.pnpm must use the same repository list when both are configured because npm and pnpm share ~/.npmrc."
}
}

locals {
Expand Down Expand Up @@ -106,13 +117,14 @@ locals {
ARTIFACTORY_EMAIL = data.coder_workspace_owner.me.email
ARTIFACTORY_ACCESS_TOKEN = data.coder_external_auth.jfrog.access_token
}
npm_repositories = length(var.package_managers.npm) > 0 ? var.package_managers.npm : var.package_managers.pnpm
npmrc = templatefile(
"${path.module}/.npmrc.tftpl",
merge(
local.common_values,
{
REPOS = [
for r in var.package_managers.npm :
for r in local.npm_repositories :
strcontains(r, ":") ? zipmap(["SCOPE", "NAME"], ["${split(":", r)[0]}:", split(":", r)[1]]) : { SCOPE = "", NAME = r }
]
}
Expand Down Expand Up @@ -145,8 +157,10 @@ resource "coder_script" "jfrog" {
{
CONFIGURE_CODE_SERVER = var.configure_code_server
HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
HAS_PNPM = length(var.package_managers.pnpm) == 0 ? "" : "YES"
NPMRC = local.npmrc
REPOSITORY_NPM = try(element(var.package_managers.npm, 0), "")
REPOSITORY_PNPM = try(element(var.package_managers.pnpm, 0), "")
HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
REPOSITORY_GO = try(element(var.package_managers.go, 0), "")
HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
Expand Down
14 changes: 13 additions & 1 deletion registry/coder/modules/jfrog-oauth/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,22 @@ if [ -z "${HAS_NPM}" ]; then
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
config_complete
fi

# Configure pnpm to use the Artifactory npm repository.
if [ -z "${HAS_PNPM}" ]; then
not_configured pnpm
else
echo "📦 Configuring pnpm..."
jf pnpmc --global --repo-resolve "${REPOSITORY_PNPM}"
config_complete
fi

if [ -n "${HAS_NPM}" ] || [ -n "${HAS_PNPM}" ]; then
cat << EOF > ~/.npmrc
${NPMRC}
Comment thread
Edd88-pixel marked this conversation as resolved.
EOF
config_complete
fi

# Configure the `pip` to use the Artifactory "python" repository.
Expand Down
20 changes: 13 additions & 7 deletions registry/coder/modules/jfrog-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ 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
package_managers = {
npm = ["npm", "@scoped:npm-scoped"]
pnpm = ["npm", "@scoped:npm-scoped"]
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
Expand All @@ -32,23 +33,26 @@ module "jfrog" {
For detailed instructions, please see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-token) on the Coder documentation.

> Note
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
> This module does not install `npm`, `pnpm`, `go`, `pip`, etc but only configures them. You need to handle the installation of these tools yourself.
>
> `jf pnpm` requires Node.js 20 or newer and pnpm 10.x or 11.x. Use JFrog CLI 2.98.0 or newer for pnpm 10.x and 2.116.0 or newer for pnpm 11.x.

![JFrog](../../.images/jfrog.png)

## Examples

### Configure npm, go, and pypi to use Artifactory local repositories
### Configure npm, pnpm, go, and pypi to use Artifactory local repositories

```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
package_managers = {
npm = ["npm-local"]
pnpm = ["npm-local"]
go = ["go-local"]
pypi = ["pypi-local"]
conda = ["conda-local"]
Expand All @@ -58,10 +62,11 @@ module "jfrog" {
}
```

You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip`, `conda`, `maven` commands.
Because npm and pnpm share `~/.npmrc`, their repository lists must match when both are configured. You should now be able to install packages from Artifactory using `jf npm`, `jf pnpm`, `jf go`, `jf pip`, and the native package-manager commands.

```shell
jf npm install prettier
jf pnpm install
jf go get github.com/golang/example/hello
jf pip install requests
conda install numpy
Expand All @@ -70,6 +75,7 @@ mvn clean install

```shell
npm install prettier
pnpm install
go get github.com/golang/example/hello
pip install requests
conda install numpy
Expand All @@ -83,7 +89,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
Expand All @@ -104,7 +110,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
Expand Down
43 changes: 43 additions & 0 deletions registry/coder/modules/jfrog-token/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,49 @@ EOF`;
);
});

it("configures pnpm with npm-compatible repositories", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
artifactory_access_token: "XXXX",
package_managers: JSON.stringify({
pnpm: ["global", "@foo:foo"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain(
'jf pnpmc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
`@foo:registry=http://${fakeFrogApi}/npm/foo`,
);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured pnpm',
);
});

it("writes one npmrc when npm and pnpm are configured", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
artifactory_access_token: "XXXX",
package_managers: JSON.stringify({
npm: ["global", "@foo:foo"],
pnpm: ["global", "@foo:foo"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
expect(coderScript.script).toContain(
'jf npmc --global --repo-resolve "global"',
);
expect(coderScript.script).toContain(
'jf pnpmc --global --repo-resolve "global"',
);
expect(
coderScript.script.match(/cat << EOF > ~\/\.npmrc/g) ?? [],
).toHaveLength(1);
});

it("generates a pip config with extra-indexes", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
Expand Down
Loading