diff --git a/.images/jfrog-oauth.png b/.images/jfrog-oauth.png new file mode 100644 index 0000000..cd897fc Binary files /dev/null and b/.images/jfrog-oauth.png differ diff --git a/jfrog-oauth/README.md b/jfrog-oauth/README.md new file mode 100644 index 0000000..00eee3d --- /dev/null +++ b/jfrog-oauth/README.md @@ -0,0 +1,77 @@ +--- +display_name: JFrog (OAuth) +description: Install the JF CLI and authenticate with Artifactory using OAuth. +icon: ../.icons/jfrog.svg +maintainer_github: coder +partner_github: jfrog +verified: true +tags: [integration, jfrog] +--- + +# JFrog + +Install the JF CLI and authenticate package managers with Artifactory using OAuth configured via the Coder `external-auth` feature. + +![JFrog OAuth](../.images/jfrog-oauth.png) + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog-oauth" + agent_id = coder_agent.example.id + jfrog_url = "https://jfrog.example.com" + auth_method = "oauth" + username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" + package_managers = { + "npm": "npm", + "go": "go", + "pypi": "pypi" + } +} +``` + +## Prerequisites + +- Coder [`external-auth`](https://docs.coder.com/docs/admin/external-auth/) configured with Artifactory. This requires a [custom integration](https://jfrog.com/help/r/jfrog-installation-setup-documentation/enable-new-integrations) in Artifactory with **Callback URL** set to `https:///external-auth/jfrog/callback`. + +## Examples + +Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username. + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog-oauth" + agent_id = coder_agent.example.id + jfrog_url = "https://jfrog.example.com" + auth_method = "oauth" + username_field = "email" + package_managers = { + "pypi": "pypi" + } +} +``` + +You should now be able to install packages from Artifactory using both the `jf pip` and `pip` command. + +```shell +jf pip install requests +``` + +```shell +pip install requests +``` + +### Using the access token in other terraform resources + +JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs). + +```hcl + +provider "docker" { + ... + registry_auth { + address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY" + username = module.jfrog.username + password = module.jfrog.access_token + } +} +``` diff --git a/jfrog-oauth/main.test.ts b/jfrog-oauth/main.test.ts new file mode 100644 index 0000000..3397eeb --- /dev/null +++ b/jfrog-oauth/main.test.ts @@ -0,0 +1,19 @@ +import { serve } from "bun"; +import { describe } from "bun:test"; +import { + createJSONResponse, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("jfrog-oauth", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: "http://localhost:8081", + package_managers: "{}", + }); +}); + +//TODO add more tests diff --git a/jfrog-oauth/main.tf b/jfrog-oauth/main.tf new file mode 100644 index 0000000..8a81594 --- /dev/null +++ b/jfrog-oauth/main.tf @@ -0,0 +1,88 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +variable "jfrog_url" { + type = string + description = "JFrog instance URL. e.g. https://jfrog.example.com" +} + +variable "username_field" { + type = string + description = "The field to use for the artifactory username. i.e. Coder username or email." + default = "username" + validation { + condition = can(regex("^(email|username)$", var.username_field)) + error_message = "username_field must be either 'email' or 'username'" + } +} + +variable "external_auth_id" { + type = string + description = "JFrog external auth ID. Default: 'jfrog'" + default = "jfrog" +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "package_managers" { + type = map(string) + description = </dev/null 2>&1; then + echo "✅ JFrog CLI is already installed, skipping installation." +else + echo "📦 Installing JFrog CLI..." + # Install the JFrog CLI. + curl -fL https://install-cli.jfrog.io | sudo sh + sudo chmod 755 /usr/local/bin/jf +fi # The jf CLI checks $CI when determining whether to use interactive # flows. @@ -14,14 +19,16 @@ export CI=true jf c rm 0 || true echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" 0 -# Configure the `npm` CLI to use the Artifactory "npm" repository. if [ -z "${REPOSITORY_NPM}" ]; then echo "🤔 REPOSITORY_NPM is not set, skipping npm configuration." else - echo "📦 Configuring npm..." - jf npmc --global --repo-resolve "${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}" + # check if npm is installed and configure it to use the Artifactory "npm" repository. + if command -v npm >/dev/null 2>&1; then + echo "📦 Configuring npm..." + jf npmc --global --repo-resolve "${REPOSITORY_NPM}" + fi cat <~/.npmrc -email = ${ARTIFACTORY_USERNAME} +email = ${ARTIFACTORY_EMAIL} registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} EOF jf rt curl /api/npm/auth >>~/.npmrc @@ -32,6 +39,7 @@ if [ -z "${REPOSITORY_PYPI}" ]; then echo "🤔 REPOSITORY_PYPI is not set, skipping pip configuration." else echo "🐍 Configuring pip..." + jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip cat <~/.pip/pip.conf [global] @@ -44,6 +52,7 @@ if [ -z "${REPOSITORY_GO}" ]; then echo "🤔 REPOSITORY_GO is not set, skipping go configuration." else echo "🐹 Configuring go..." + jf go-config --global --repo-resolve "${REPOSITORY_GO}" export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}" fi echo "🥳 Configuration complete!" diff --git a/jfrog-token/README.md b/jfrog-token/README.md new file mode 100644 index 0000000..512c863 --- /dev/null +++ b/jfrog-token/README.md @@ -0,0 +1,86 @@ +--- +display_name: JFrog (Token) +description: Install the JF CLI and authenticate with Artifactory using Artifactory terraform provider. +icon: ../.icons/jfrog.svg +maintainer_github: coder +partner_github: jfrog +verified: true +tags: [integration, jfrog] +--- + +# JFrog + +Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider. + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog-token" + agent_id = coder_agent.example.id + jfrog_url = "https://XXXX.jfrog.io" + artifactory_access_token = var.artifactory_access_token + package_managers = { + "npm": "npm", + "go": "go", + "pypi": "pypi" + } +} +``` + +Get a JFrog access token from your Artifactory instance. The token must be an [admin token](https://registry.terraform.io/providers/jfrog/artifactory/latest/docs#access-token). It is recommended to store the token in a secret terraform variable. + +```hcl +variable "artifactory_access_token" { + type = string + sensitive = true +} +``` + +![JFrog](../.images/jfrog.png) + +## Examples + +### Configure npm, go, and pypi to use Artifactory local repositories + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog-token" + agent_id = coder_agent.example.id + jfrog_url = "https://YYYY.jfrog.io" + artifactory_access_token = var.artifactory_access_token # An admin access token + package_managers = { + "npm": "npm-local", + "go": "go-local", + "pypi": "pypi-local" + } +} +``` + +You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip` commands. + +```shell +jf npm install prettier +jf go get github.com/golang/example/hello +jf pip install requests +``` + +```shell +npm install prettier +go get github.com/golang/example/hello +pip install requests +``` + +### Using the access token in other terraform resources + +JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs). + +```hcl + +provider "docker" { + ... + registry_auth { + address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY" + username = module.jfrog.username + password = module.jfrog.access_token + } +} +``` diff --git a/jfrog/main.test.ts b/jfrog-token/main.test.ts similarity index 94% rename from jfrog/main.test.ts rename to jfrog-token/main.test.ts index 82ad38b..b3b8df9 100644 --- a/jfrog/main.test.ts +++ b/jfrog-token/main.test.ts @@ -6,7 +6,7 @@ import { testRequiredVariables, } from "../test"; -describe("jfrog", async () => { +describe("jfrog-token", async () => { await runTerraformInit(import.meta.dir); // Run a fake JFrog server so the provider can initialize @@ -25,7 +25,7 @@ describe("jfrog", async () => { return createJSONResponse({ token_id: "xxx", access_token: "xxx", - scope: "any", + scopes: "any", }); return createJSONResponse({}); }, diff --git a/jfrog-token/main.tf b/jfrog-token/main.tf new file mode 100644 index 0000000..d9aa55f --- /dev/null +++ b/jfrog-token/main.tf @@ -0,0 +1,121 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + artifactory = { + source = "registry.terraform.io/jfrog/artifactory" + version = "~> 9.8.0" + } + } +} + +variable "jfrog_url" { + type = string + description = "JFrog instance URL. e.g. https://YYY.jfrog.io" +} + +variable "artifactory_access_token" { + type = string + description = "The admin-level access token to use for JFrog." +} + +variable "check_license" { + type = bool + description = "Toggle for pre-flight checking of Artifactory license. Default to `true`." + default = true +} + +variable "refreshable" { + type = bool + description = "Is this token refreshable? Default is `false`." + default = false +} + +variable "expires_in" { + type = number + description = "The amount of time, in seconds, it would take for the token to expire." + default = null +} + +variable "username_field" { + type = string + description = "The field to use for the artifactory username. Default `username`." + default = "username" + validation { + condition = can(regex("^(email|username)$", var.username_field)) + error_message = "username_field must be either 'email' or 'username'" + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "package_managers" { + type = map(string) + description = < 0 ? local.username : "dummy" + scopes = ["applied-permissions/user"] + refreshable = var.refreshable + expires_in = var.expires_in +} + +data "coder_workspace" "me" {} + +resource "coder_script" "jfrog" { + agent_id = var.agent_id + display_name = "jfrog" + icon = "/icon/jfrog.svg" + script = templatefile("${path.module}/run.sh", { + JFROG_URL : var.jfrog_url, + JFROG_HOST : replace(var.jfrog_url, "https://", ""), + ARTIFACTORY_USERNAME : local.username, + ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email, + ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token, + REPOSITORY_NPM : lookup(var.package_managers, "npm", ""), + REPOSITORY_GO : lookup(var.package_managers, "go", ""), + REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""), + }) + run_on_start = true +} + +output "access_token" { + description = "value of the JFrog access token" + value = artifactory_scoped_token.me.access_token + sensitive = true +} + +output "username" { + description = "value of the JFrog username" + value = local.username +} diff --git a/jfrog-token/run.sh b/jfrog-token/run.sh new file mode 100644 index 0000000..efba187 --- /dev/null +++ b/jfrog-token/run.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' + +# check if JFrog CLI is already installed +if command -v jf >/dev/null 2>&1; then + echo "✅ JFrog CLI is already installed, skipping installation." +else + echo "📦 Installing JFrog CLI..." + # Install the JFrog CLI. + curl -fL https://install-cli.jfrog.io | sudo sh + sudo chmod 755 /usr/local/bin/jf +fi + +# The jf CLI checks $CI when determining whether to use interactive +# flows. +export CI=true +# Authenticate with the JFrog CLI. +jf c rm 0 || true +echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" 0 + +if [ -z "${REPOSITORY_NPM}" ]; then + echo "🤔 REPOSITORY_NPM is not set, skipping npm configuration." +else + # check if npm is installed and configure it to use the Artifactory "npm" repository. + if command -v npm >/dev/null 2>&1; then + echo "📦 Configuring npm..." + jf npmc --global --repo-resolve "${REPOSITORY_NPM}" + fi + cat <~/.npmrc +email = ${ARTIFACTORY_EMAIL} +registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} +EOF + jf rt curl /api/npm/auth >>~/.npmrc +fi + +# Configure the `pip` to use the Artifactory "python" repository. +if [ -z "${REPOSITORY_PYPI}" ]; then + echo "🤔 REPOSITORY_PYPI is not set, skipping pip configuration." +else + echo "🐍 Configuring pip..." + jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" + mkdir -p ~/.pip + cat <~/.pip/pip.conf +[global] +index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple +EOF +fi + +# Set GOPROXY to use the Artifactory "go" repository. +if [ -z "${REPOSITORY_GO}" ]; then + echo "🤔 REPOSITORY_GO is not set, skipping go configuration." +else + echo "🐹 Configuring go..." + jf go-config --global --repo-resolve "${REPOSITORY_GO}" + export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}" +fi +echo "🥳 Configuration complete!" diff --git a/jfrog/README.md b/jfrog/README.md deleted file mode 100644 index 9ae7cce..0000000 --- a/jfrog/README.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -display_name: JFrog -description: Install the JF CLI and authenticate with Artifactory -icon: ../.icons/jfrog.svg -maintainer_github: coder -partner_github: jfrog -verified: true -tags: [integration] ---- - -# JFrog - -Install the JF CLI and authenticate package managers with Artifactory. - -```hcl -module "jfrog" { - source = "https://registry.coder.com/modules/jfrog" - agent_id = coder_agent.example.id - jfrog_url = "https://YYYY.jfrog.io" - artifactory_access_token = var.artifactory_access_token # An admin access token - package_managers = { - "npm": "npm-remote", - "go": "go-remote", - "pypi": "pypi-remote" - } -} -``` - -Get a JFrog access token from your Artifactory instance. The token must have admin permissions. It is recommended to store the token in a secret terraform variable. - -```hcl -variable "artifactory_access_token" { - type = string - sensitive = true -} -``` - -![JFrog](../.images/jfrog.png) - -## Examples - -### Configure npm, go, and pypi to use Artifactory local repositories - -```hcl -module "jfrog" { - source = "https://registry.coder.com/modules/jfrog" - agent_id = coder_agent.example.id - jfrog_url = "https://YYYY.jfrog.io" - artifactory_access_token = var.artifactory_access_token # An admin access token - package_managers = { - "npm": "npm-local", - "go": "go-local", - "pypi": "pypi-local" - } -} -``` diff --git a/jfrog/main.tf b/jfrog/main.tf deleted file mode 100644 index 807bdf8..0000000 --- a/jfrog/main.tf +++ /dev/null @@ -1,71 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - coder = { - source = "coder/coder" - version = ">= 0.12" - } - artifactory = { - source = "registry.terraform.io/jfrog/artifactory" - version = "~> 8.4.0" - } - } -} - -variable "jfrog_url" { - type = string - description = "JFrog instance URL. e.g. https://YYY.jfrog.io" -} - -variable "artifactory_access_token" { - type = string - description = "The admin-level access token to use for JFrog." -} - -# Configure the Artifactory provider -provider "artifactory" { - url = join("/", [var.jfrog_url, "artifactory"]) - access_token = var.artifactory_access_token -} -resource "artifactory_scoped_token" "me" { - # This is hacky, but on terraform plan the data source gives empty strings, - # which fails validation. - username = length(data.coder_workspace.me.owner_email) > 0 ? data.coder_workspace.me.owner_email : "plan" -} - -variable "agent_id" { - type = string - description = "The ID of a Coder agent." -} - -variable "package_managers" { - type = map(string) - description = <