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..f7ea867 --- /dev/null +++ b/jfrog-oauth/README.md @@ -0,0 +1,61 @@ +--- +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 +``` diff --git a/jfrog-oauth/main.test.ts b/jfrog-oauth/main.test.ts new file mode 100644 index 0000000..f31aa5d --- /dev/null +++ b/jfrog-oauth/main.test.ts @@ -0,0 +1,17 @@ +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: "{}", + }); +}); diff --git a/jfrog-oauth/main.tf b/jfrog-oauth/main.tf new file mode 100644 index 0000000..b6f1583 --- /dev/null +++ b/jfrog-oauth/main.tf @@ -0,0 +1,77 @@ +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/README.md b/jfrog-token/README.md similarity index 63% rename from jfrog/README.md rename to jfrog-token/README.md index 9ae7cce..291111f 100644 --- a/jfrog/README.md +++ b/jfrog-token/README.md @@ -1,27 +1,27 @@ --- -display_name: JFrog -description: Install the JF CLI and authenticate with Artifactory +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] +tags: [integration, jfrog] --- # JFrog -Install the JF CLI and authenticate package managers with Artifactory. +Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider. ```hcl module "jfrog" { - source = "https://registry.coder.com/modules/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-remote", - "go": "go-remote", - "pypi": "pypi-remote" + "npm": "npm", + "go": "go", + "pypi": "pypi" } } ``` @@ -43,7 +43,7 @@ variable "artifactory_access_token" { ```hcl module "jfrog" { - source = "https://registry.coder.com/modules/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 @@ -54,3 +54,17 @@ 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` 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 +``` 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/main.tf b/jfrog-token/main.tf similarity index 69% rename from jfrog/main.tf rename to jfrog-token/main.tf index 807bdf8..a586148 100644 --- a/jfrog/main.tf +++ b/jfrog-token/main.tf @@ -8,7 +8,7 @@ terraform { } artifactory = { source = "registry.terraform.io/jfrog/artifactory" - version = "~> 8.4.0" + version = "~> 9.8.0" } } } @@ -23,15 +23,14 @@ variable "artifactory_access_token" { 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 "username_field" { + type = string + description = "The field to use for the artifactory username. i.e. Coder username or email." + default = "email" + validation { + condition = can(regex("^(email|username)$", var.username_field)) + error_message = "username_field must be either 'email' or 'username'" + } } variable "agent_id" { @@ -52,6 +51,25 @@ For example: EOF } +locals { + # The username field to use for artifactory + username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner +} + +# 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(local.username) > 0 ? local.username : "dummy" + scopes = ["applied-permissions/user"] + refreshable = true +} + data "coder_workspace" "me" {} resource "coder_script" "jfrog" { @@ -61,7 +79,8 @@ resource "coder_script" "jfrog" { script = templatefile("${path.module}/run.sh", { JFROG_URL : var.jfrog_url, JFROG_HOST : replace(var.jfrog_url, "https://", ""), - ARTIFACTORY_USERNAME : data.coder_workspace.me.owner_email, + 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", ""), 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!"