diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 960cd03..4d0c709 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Setup @@ -27,7 +27,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 + with: + fetch-depth: 0 # Needed to get tags + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Setup @@ -38,3 +40,16 @@ jobs: uses: crate-ci/typos@v1.17.2 - name: Lint run: bun lint + - name: Check version + shell: bash + run: | + # check for version changes + ./update-version.sh + # Check if any changes were made in README.md files + if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then + echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files." + git diff -- '**/README.md' + exit 1 + else + echo "No version mismatch detected. All versions are up to date." + fi diff --git a/.github/workflows/update-readme.yaml b/.github/workflows/update-readme.yaml deleted file mode 100644 index 0d0e226..0000000 --- a/.github/workflows/update-readme.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: Update README on Tag - -on: - workflow_dispatch: - push: - tags: - - 'v*' - -jobs: - update-readme: - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get the latest tag - id: get-latest-tag - run: echo "TAG=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_OUTPUT - - - name: Run update script - run: ./update-version.sh - - - name: Create Pull Request - id: create-pr - uses: peter-evans/create-pull-request@v5 - with: - commit-message: 'chore: bump version to ${{ env.TAG }} in README.md files' - title: 'chore: bump version to ${{ env.TAG }} in README.md files' - body: 'This is an auto-generated PR to update README.md files of all modules with the new tag ${{ env.TAG }}' - branch: 'update-readme-branch' - base: 'main' - env: - TAG: ${{ steps.get-latest-tag.outputs.TAG }} - - - name: Auto-approve - uses: hmarr/auto-approve-action@v4 - if: github.ref == 'refs/heads/update-readme-branch' diff --git a/cursor/README.md b/cursor/README.md index a62743b..c2997be 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder) ```tf module "cursor" { source = "registry.coder.com/modules/cursor/coder" - version = "1.0.18" + version = "1.0.19" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "cursor" { ```tf module "cursor" { source = "registry.coder.com/modules/cursor/coder" - version = "1.0.18" + version = "1.0.19" agent_id = coder_agent.example.id folder = "/home/coder/project" } diff --git a/cursor/main.tf b/cursor/main.tf index 4d48191..f350f94 100644 --- a/cursor/main.tf +++ b/cursor/main.tf @@ -40,7 +40,7 @@ resource "coder_app" "cursor" { external = true icon = "/icon/cursor.svg" slug = "cursor" - display_name = "Cursor IDE" + display_name = "Cursor Desktop" order = var.order url = join("", [ "cursor://coder.coder-remote/open", diff --git a/filebrowser/README.md b/filebrowser/README.md index dd26d27..8665d82 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -13,10 +13,9 @@ A file browser for your workspace. ```tf module "filebrowser" { - source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.18" - agent_id = coder_agent.example.id - agent_name = "main" + source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.19" + agent_id = coder_agent.example.id } ``` @@ -28,11 +27,10 @@ module "filebrowser" { ```tf module "filebrowser" { - source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.18" - agent_id = coder_agent.example.id - agent_name = "main" - folder = "/home/coder/project" + source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + folder = "/home/coder/project" } ``` @@ -41,9 +39,8 @@ module "filebrowser" { ```tf module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.18" + version = "1.0.19" agent_id = coder_agent.example.id - agent_name = "main" database_path = ".config/filebrowser.db" } ``` diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts index ff6d045..7dd4972 100644 --- a/filebrowser/main.test.ts +++ b/filebrowser/main.test.ts @@ -11,13 +11,11 @@ describe("filebrowser", async () => { testRequiredVariables(import.meta.dir, { agent_id: "foo", - agent_name: "main", }); it("fails with wrong database_path", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - agent_name: "main", database_path: "nofb", }).catch((e) => { if (!e.message.startsWith("\nError: Invalid value for variable")) { @@ -29,7 +27,6 @@ describe("filebrowser", async () => { it("runs with default", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - agent_name: "main", }); const output = await executeScriptInContainer(state, "alpine"); expect(output.exitCode).toBe(0); @@ -51,7 +48,6 @@ describe("filebrowser", async () => { it("runs with database_path var", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - agent_name: "main", database_path: ".config/filebrowser.db", }); const output = await executeScriptInContainer(state, "alpine"); @@ -74,7 +70,6 @@ describe("filebrowser", async () => { it("runs with folder var", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - agent_name: "main", folder: "/home/coder/project", }); const output = await executeScriptInContainer(state, "alpine"); diff --git a/filebrowser/main.tf b/filebrowser/main.tf index e6b88c6..4fd7459 100644 --- a/filebrowser/main.tf +++ b/filebrowser/main.tf @@ -21,6 +21,12 @@ data "coder_workspace_owner" "me" {} variable "agent_name" { type = string description = "The name of the main deployment. (Used to build the subpath for coder_app.)" + default = "" + validation { + # If subdomain is false, then agent_name must be set. + condition = var.subdomain || var.agent_name != "" + error_message = "The agent_name must be set." + } } variable "database_path" { diff --git a/jfrog-oauth/.npmrc.tftpl b/jfrog-oauth/.npmrc.tftpl new file mode 100644 index 0000000..8bb9fb8 --- /dev/null +++ b/jfrog-oauth/.npmrc.tftpl @@ -0,0 +1,5 @@ +email=${ARTIFACTORY_EMAIL} +%{ for REPO in REPOS ~} +${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME} +//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN} +%{ endfor ~} diff --git a/jfrog-oauth/README.md b/jfrog-oauth/README.md index b7f9d58..4423a74 100644 --- a/jfrog-oauth/README.md +++ b/jfrog-oauth/README.md @@ -17,15 +17,16 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-oauth/coder" - version = "1.0.15" + version = "1.0.19" agent_id = coder_agent.example.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", - "go" : "go", - "pypi" : "pypi" + npm = ["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"] } } ``` @@ -44,13 +45,13 @@ Configure the Python pip package manager to fetch packages from Artifactory whil ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-oauth/coder" - version = "1.0.15" + version = "1.0.19" agent_id = coder_agent.example.id jfrog_url = "https://example.jfrog.io" username_field = "email" package_managers = { - "pypi" : "pypi" + pypi = ["pypi"] } } ``` @@ -72,15 +73,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-oauth/coder" - version = "1.0.15" + version = "1.0.19" agent_id = coder_agent.example.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" configure_code_server = true # Add JFrog extension configuration for code-server package_managers = { - "npm" : "npm", - "go" : "go", - "pypi" : "pypi" + npm = ["npm"] + go = ["go"] + pypi = ["pypi"] } } ``` diff --git a/jfrog-oauth/main.test.ts b/jfrog-oauth/main.test.ts index e3a26eb..da8b9bf 100644 --- a/jfrog-oauth/main.test.ts +++ b/jfrog-oauth/main.test.ts @@ -1,15 +1,129 @@ -/** - * @todo Add more tests - */ -import { describe } from "bun:test"; -import { runTerraformInit, testRequiredVariables } from "../test"; +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); - testRequiredVariables(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: "http://localhost:8081", - package_managers: "{}", + const fakeFrogApi = "localhost:8081/artifactory/api"; + const fakeFrogUrl = "http://localhost:8081"; + const user = "default"; + + it("can run apply with required variables", async () => { + 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', + ); }); }); diff --git a/jfrog-oauth/main.tf b/jfrog-oauth/main.tf index 767235a..0bc2256 100644 --- a/jfrog-oauth/main.tf +++ b/jfrog-oauth/main.tf @@ -53,23 +53,51 @@ variable "configure_code_server" { } variable "package_managers" { - type = map(string) - description = < /dev/null 2>&1; then echo "✅ JFrog CLI is already installed, skipping installation." @@ -20,52 +35,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO jf c use "${JFROG_SERVER_ID}" # Configure npm to use the Artifactory "npm" repository. -if [ -z "${REPOSITORY_NPM}" ]; then - echo "🤔 no npm repository is set, skipping npm configuration." - echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input." +if [ -z "${HAS_NPM}" ]; then + not_configured npm else echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc -email=${ARTIFACTORY_EMAIL} -registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} +${NPMRC} EOF - echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc + config_complete fi # Configure the `pip` to use the Artifactory "python" repository. -if [ -z "${REPOSITORY_PYPI}" ]; then - echo "🤔 no pypi repository is set, skipping pip configuration." - echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input." +if [ -z "${HAS_PYPI}" ]; then + not_configured pypi else - echo "📦 Configuring pip..." + echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip cat << EOF > ~/.pip/pip.conf -[global] -index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple +${PIP_CONF} EOF + config_complete fi # Configure Artifactory "go" repository. -if [ -z "${REPOSITORY_GO}" ]; then - echo "🤔 no go repository is set, skipping go configuration." - echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input." +if [ -z "${HAS_GO}" ]; then + not_configured go else echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" + config_complete fi -echo "🥳 Configuration complete!" # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ -z "${REPOSITORY_DOCKER}" ]; then - echo "🤔 no docker repository is set, skipping docker configuration." - echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input." +if [ -z "${HAS_DOCKER}" ]; then + not_configured docker else if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker - echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin + ${REGISTER_DOCKER} else echo "🤔 no docker is installed, skipping docker configuration." fi @@ -96,20 +106,19 @@ echo "📦 Configuring JFrog CLI completion..." SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}') # Generate the completion script jf completion $SHELLNAME --install +begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" # Add the completion script to the user's shell profile if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then - if ! grep -q "# jf CLI shell completion" ~/.bashrc; then - echo "" >> ~/.bashrc - echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.bashrc + if ! grep -q "$begin_stanza" ~/.bashrc; then + printf "%s\n" "$begin_stanza" >> ~/.bashrc echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc echo "# END: jf CLI shell completion" >> ~/.bashrc else echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping." fi elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then - if ! grep -q "# jf CLI shell completion" ~/.zshrc; then - echo "" >> ~/.zshrc - echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.zshrc + if ! grep -q "$begin_stanza" ~/.zshrc; then + printf "\n%s\n" "$begin_stanza" >> ~/.zshrc echo "autoload -Uz compinit" >> ~/.zshrc echo "compinit" >> ~/.zshrc echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc diff --git a/jfrog-token/.npmrc.tftpl b/jfrog-token/.npmrc.tftpl new file mode 100644 index 0000000..8bb9fb8 --- /dev/null +++ b/jfrog-token/.npmrc.tftpl @@ -0,0 +1,5 @@ +email=${ARTIFACTORY_EMAIL} +%{ for REPO in REPOS ~} +${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME} +//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN} +%{ endfor ~} diff --git a/jfrog-token/README.md b/jfrog-token/README.md index f903f90..146dc7f 100644 --- a/jfrog-token/README.md +++ b/jfrog-token/README.md @@ -15,14 +15,15 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.15" + version = "1.0.19" 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" + npm = ["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"] } } ``` @@ -41,14 +42,14 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.15" + version = "1.0.19" 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" + npm = ["npm-local"] + go = ["go-local"] + pypi = ["pypi-local"] } } ``` @@ -74,15 +75,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio ```tf module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.15" + version = "1.0.19" agent_id = coder_agent.example.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token configure_code_server = true # Add JFrog extension configuration for code-server package_managers = { - "npm" : "npm", - "go" : "go", - "pypi" : "pypi" + npm = ["npm"] + go = ["go"] + pypi = ["pypi"] } } ``` @@ -94,15 +95,13 @@ data "coder_workspace" "me" {} module "jfrog" { source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.15" + version = "1.0.19" agent_id = coder_agent.example.id jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}" package_managers = { - "npm" : "npm", - "go" : "go", - "pypi" : "pypi" + npm = ["npm"] } } ``` diff --git a/jfrog-token/main.test.ts b/jfrog-token/main.test.ts index be5a7be..a6fe6bc 100644 --- a/jfrog-token/main.test.ts +++ b/jfrog-token/main.test.ts @@ -1,12 +1,29 @@ import { serve } from "bun"; -import { describe } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { createJSONResponse, + findResourceInstance, runTerraformInit, + runTerraformApply, testRequiredVariables, } from "../test"; describe("jfrog-token", async () => { + type TestVariables = { + agent_id: string; + jfrog_url: string; + artifactory_access_token: string; + package_managers: string; + + token_description?: string; + check_license?: boolean; + refreshable?: boolean; + expires_in?: number; + username_field?: string; + jfrog_server_id?: string; + configure_code_server?: boolean; + }; + await runTerraformInit(import.meta.dir); // Run a fake JFrog server so the provider can initialize @@ -32,10 +49,116 @@ describe("jfrog-token", async () => { port: 0, }); - testRequiredVariables(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: `http://${fakeFrogHost.hostname}:${fakeFrogHost.port}`, - artifactory_access_token: "XXXX", - package_managers: "{}", + const fakeFrogApi = `${fakeFrogHost.hostname}:${fakeFrogHost.port}/artifactory/api`; + const fakeFrogUrl = `http://${fakeFrogHost.hostname}:${fakeFrogHost.port}`; + const user = "default"; + const token = "xxx"; + + it("can run apply with required variables", async () => { + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: fakeFrogUrl, + artifactory_access_token: "XXXX", + 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, + artifactory_access_token: "XXXX", + 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=xxx +@foo:registry=http://${fakeFrogApi}/npm/foo +//${fakeFrogApi}/npm/foo/:_authToken=xxx +@bar:registry=http://${fakeFrogApi}/npm/bar +//${fakeFrogApi}/npm/bar/:_authToken=xxx + +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, + artifactory_access_token: "XXXX", + 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}:${token}@${fakeFrogApi}/pypi/global/simple +extra-index-url = + https://${user}:${token}@${fakeFrogApi}/pypi/foo/simple + https://${user}:${token}@${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, + artifactory_access_token: "XXXX", + 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, + artifactory_access_token: "XXXX", + package_managers: JSON.stringify({ + go: ["foo", "bar", "baz"], + }), + }); + const proxyEnv = findResourceInstance(state, "coder_env", "goproxy"); + const proxies = ["foo", "bar", "baz"] + .map((r) => `https://${user}:${token}@${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', + ); }); }); diff --git a/jfrog-token/main.tf b/jfrog-token/main.tf index 90dad61..f6f5f5b 100644 --- a/jfrog-token/main.tf +++ b/jfrog-token/main.tf @@ -80,23 +80,51 @@ variable "configure_code_server" { } variable "package_managers" { - type = map(string) - description = < /dev/null 2>&1; then echo "✅ JFrog CLI is already installed, skipping installation." @@ -11,8 +26,7 @@ else sudo chmod 755 /usr/local/bin/jf fi -# The jf CLI checks $CI when determining whether to use interactive -# flows. +# The jf CLI checks $CI when determining whether to use interactive flows. export CI=true # Authenticate JFrog CLI with Artifactory. echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}" @@ -20,52 +34,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO jf c use "${JFROG_SERVER_ID}" # Configure npm to use the Artifactory "npm" repository. -if [ -z "${REPOSITORY_NPM}" ]; then - echo "🤔 no npm repository is set, skipping npm configuration." - echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input." +if [ -z "${HAS_NPM}" ]; then + not_configured npm else echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${REPOSITORY_NPM}" cat << EOF > ~/.npmrc -email=${ARTIFACTORY_EMAIL} -registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} +${NPMRC} EOF - echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc + config_complete fi # Configure the `pip` to use the Artifactory "python" repository. -if [ -z "${REPOSITORY_PYPI}" ]; then - echo "🤔 no pypi repository is set, skipping pip configuration." - echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input." +if [ -z "${HAS_PYPI}" ]; then + not_configured pypi else echo "🐍 Configuring pip..." jf pipc --global --repo-resolve "${REPOSITORY_PYPI}" mkdir -p ~/.pip cat << EOF > ~/.pip/pip.conf -[global] -index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple +${PIP_CONF} EOF + config_complete fi # Configure Artifactory "go" repository. -if [ -z "${REPOSITORY_GO}" ]; then - echo "🤔 no go repository is set, skipping go configuration." - echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input." +if [ -z "${HAS_GO}" ]; then + not_configured go else echo "🐹 Configuring go..." jf goc --global --repo-resolve "${REPOSITORY_GO}" + config_complete fi -echo "🥳 Configuration complete!" # Configure the JFrog CLI to use the Artifactory "docker" repository. -if [ -z "${REPOSITORY_DOCKER}" ]; then - echo "🤔 no docker repository is set, skipping docker configuration." - echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input." +if [ -z "${HAS_DOCKER}" ]; then + not_configured docker else if command -v docker > /dev/null 2>&1; then echo "🔑 Configuring 🐳 docker credentials..." mkdir -p ~/.docker - echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin + ${REGISTER_DOCKER} else echo "🤔 no docker is installed, skipping docker configuration." fi @@ -96,20 +105,19 @@ echo "📦 Configuring JFrog CLI completion..." SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}') # Generate the completion script jf completion $SHELLNAME --install +begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" # Add the completion script to the user's shell profile if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then - if ! grep -q "# jf CLI shell completion" ~/.bashrc; then - echo "" >> ~/.bashrc - echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.bashrc + if ! grep -q "$begin_stanza" ~/.bashrc; then + printf "%s\n" "$begin_stanza" >> ~/.bashrc echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc echo "# END: jf CLI shell completion" >> ~/.bashrc else echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping." fi elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then - if ! grep -q "# jf CLI shell completion" ~/.zshrc; then - echo "" >> ~/.zshrc - echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.zshrc + if ! grep -q "$begin_stanza" ~/.zshrc; then + printf "\n%s\n" "$begin_stanza" >> ~/.zshrc echo "autoload -Uz compinit" >> ~/.zshrc echo "compinit" >> ~/.zshrc echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc diff --git a/jupyter-notebook/README.md b/jupyter-notebook/README.md index 6338f11..83d36cb 100644 --- a/jupyter-notebook/README.md +++ b/jupyter-notebook/README.md @@ -16,7 +16,7 @@ A module that adds Jupyter Notebook in your Coder template. ```tf module "jupyter-notebook" { source = "registry.coder.com/modules/jupyter-notebook/coder" - version = "1.0.8" + version = "1.0.19" agent_id = coder_agent.example.id } ``` diff --git a/jupyter-notebook/run.sh b/jupyter-notebook/run.sh index 4f8c4a2..0c7a9b8 100755 --- a/jupyter-notebook/run.sh +++ b/jupyter-notebook/run.sh @@ -7,14 +7,14 @@ printf "$${BOLD}Installing jupyter-notebook!\n" # check if jupyter-notebook is installed if ! command -v jupyter-notebook > /dev/null 2>&1; then # install jupyter-notebook - # check if python3 pip is installed - if ! command -v pip3 > /dev/null 2>&1; then - echo "pip3 is not installed" - echo "Please install pip3 in your Dockerfile/VM image before running this script" + # check if pipx is installed + if ! command -v pipx > /dev/null 2>&1; then + echo "pipx is not installed" + echo "Please install pipx in your Dockerfile/VM image before using this module" exit 1 fi - # install jupyter-notebook - pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyter + # install jupyter notebook + pipx install -q notebook echo "🥳 jupyter-notebook has been installed\n\n" else echo "🥳 jupyter-notebook is already installed\n\n" @@ -22,4 +22,4 @@ fi echo "👷 Starting jupyter-notebook in background..." echo "check logs at ${LOG_PATH}" -$HOME/.local/bin/jupyter notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & +$HOME/.local/bin/jupyter-notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & diff --git a/jupyterlab/README.md b/jupyterlab/README.md index 3d04cf3..ed73b56 100644 --- a/jupyterlab/README.md +++ b/jupyterlab/README.md @@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template. ```tf module "jupyterlab" { source = "registry.coder.com/modules/jupyterlab/coder" - version = "1.0.8" + version = "1.0.19" agent_id = coder_agent.example.id } ``` diff --git a/jupyterlab/main.test.ts b/jupyterlab/main.test.ts index 57db24c..cf9ac1f 100644 --- a/jupyterlab/main.test.ts +++ b/jupyterlab/main.test.ts @@ -22,7 +22,7 @@ const executeScriptInContainerWithPip = async ( }> => { const instance = findResourceInstance(state, "coder_script"); const id = await runContainer(image); - const respPip = await execContainer(id, [shell, "-c", "apk add py3-pip"]); + const respPipx = await execContainer(id, [shell, "-c", "apk add pipx"]); const resp = await execContainer(id, [shell, "-c", instance.script]); const stdout = resp.stdout.trim().split("\n"); const stderr = resp.stderr.trim().split("\n"); @@ -40,7 +40,7 @@ describe("jupyterlab", async () => { agent_id: "foo", }); - it("fails without pip3", async () => { + it("fails without pipx", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", }); @@ -48,14 +48,14 @@ describe("jupyterlab", async () => { expect(output.exitCode).toBe(1); expect(output.stdout).toEqual([ "\u001B[0;1mInstalling jupyterlab!", - "pip3 is not installed", - "Please install pip3 in your Dockerfile/VM image before running this script", + "pipx is not installed", + "Please install pipx in your Dockerfile/VM image before running this script", ]); }); - // TODO: Add faster test to run with pip3. + // TODO: Add faster test to run with pipx. // currently times out. - // it("runs with pip3", async () => { + // it("runs with pipx", async () => { // ... // const output = await executeScriptInContainerWithPip(state, "alpine"); // ... diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh index b040cec..0245b06 100755 --- a/jupyterlab/run.sh +++ b/jupyterlab/run.sh @@ -7,14 +7,14 @@ printf "$${BOLD}Installing jupyterlab!\n" # check if jupyterlab is installed if ! command -v jupyterlab > /dev/null 2>&1; then # install jupyterlab - # check if python3 pip is installed - if ! command -v pip3 > /dev/null 2>&1; then - echo "pip3 is not installed" - echo "Please install pip3 in your Dockerfile/VM image before running this script" + # check if pipx is installed + if ! command -v pipx > /dev/null 2>&1; then + echo "pipx is not installed" + echo "Please install pipx in your Dockerfile/VM image before running this script" exit 1 fi # install jupyterlab - pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyterlab + pipx install -q jupyterlab echo "🥳 jupyterlab has been installed\n\n" else echo "🥳 jupyterlab is already installed\n\n" @@ -22,4 +22,4 @@ fi echo "👷 Starting jupyterlab in background..." echo "check logs at ${LOG_PATH}" -$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & +$HOME/.local/bin/jupyter-lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & diff --git a/terraform_validate.sh b/terraform_validate.sh index 292c94c..492e65a 100755 --- a/terraform_validate.sh +++ b/terraform_validate.sh @@ -4,25 +4,25 @@ set -euo pipefail # Function to run terraform init and validate in a directory run_terraform() { - local dir="$1" - echo "Running terraform init and validate in $dir" - pushd "$dir" - terraform init -upgrade - terraform validate - popd + local dir="$1" + echo "Running terraform init and validate in $dir" + pushd "$dir" + terraform init -upgrade + terraform validate + popd } # Main script main() { - # Get the directory of the script - script_dir=$(dirname "$(readlink -f "$0")") + # Get the directory of the script + script_dir=$(dirname "$(readlink -f "$0")") - # Get all subdirectories in the repository - subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort) + # Get all subdirectories in the repository + subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort) - for dir in $subdirs; do - run_terraform "$dir" - done + for dir in $subdirs; do + run_terraform "$dir" + done } # Run the main script diff --git a/test.ts b/test.ts index 9c10366..5437374 100644 --- a/test.ts +++ b/test.ts @@ -109,6 +109,8 @@ export interface TerraformState { resources: [TerraformStateResource, ...TerraformStateResource[]]; } +type TerraformVariables = Record; + export interface CoderScriptAttributes { script: string; agent_id: string; @@ -147,9 +149,9 @@ export const findResourceInstance = ( * Creates a test-case for each variable provided and ensures that the apply * fails without it. */ -export const testRequiredVariables = >( +export const testRequiredVariables = ( dir: string, - vars: TVars, + vars: Readonly, ) => { // Ensures that all required variables are provided. it("required variables", async () => { @@ -159,8 +161,8 @@ export const testRequiredVariables = >( const varNames = Object.keys(vars); for (const varName of varNames) { // Ensures that every variable provided is required! - it(`missing variable ${varName}`, async () => { - const localVars: Record = {}; + it(`missing variable: ${varName}`, async () => { + const localVars: TerraformVariables = {}; for (const otherVarName of varNames) { if (otherVarName !== varName) { localVars[otherVarName] = vars[otherVarName]; @@ -189,11 +191,9 @@ export const testRequiredVariables = >( * fine to run in parallel with other instances of this function, as it uses a * random state file. */ -export const runTerraformApply = async < - TVars extends Readonly>, ->( +export const runTerraformApply = async ( dir: string, - vars: TVars, + vars: Readonly, env?: Record, ): Promise => { const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; diff --git a/tsconfig.json b/tsconfig.json index dd38e58..5de8ae3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "esnext", - "module": "esnext", + "module": "nodenext", "strict": true, "allowSyntheticDefaultImports": true, "moduleResolution": "nodenext", diff --git a/update-version.sh b/update-version.sh index 5deb63b..b062736 100755 --- a/update-version.sh +++ b/update-version.sh @@ -1,20 +1,24 @@ #!/usr/bin/env bash -# This script updates the version number in the README.md files of all modules -# to the latest tag in the repository. It is intended to be run from the root +# This script increments the version number in the README.md files of all modules +# by 1 patch version. It is intended to be run from the root # of the repository or by using the `bun update-version` command. set -euo pipefail current_tag=$(git describe --tags --abbrev=0) -previous_tag=$(git describe --tags --abbrev=0 $current_tag^) -mapfile -t changed_dirs < <(git diff --name-only "$previous_tag"..."$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u) -LATEST_TAG=$(git describe --abbrev=0 --tags | sed 's/^v//') || exit $? +# Increment the patch version +LATEST_TAG=$(echo "$current_tag" | sed 's/^v//' | awk -F. '{print $1"."$2"."$3+1}') || exit $? +# List directories with changes that are not README.md or test files +mapfile -t changed_dirs < <(git diff --name-only "$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u) + +echo "Directories with changes: ${changed_dirs[*]}" + +# Iterate over directories and update version in README.md for dir in "${changed_dirs[@]}"; do if [[ -f "$dir/README.md" ]]; then - echo "Bumping version in $dir/README.md" file="$dir/README.md" tmpfile=$(mktemp /tmp/tempfile.XXXXXX) awk -v tag="$LATEST_TAG" '{ @@ -25,5 +29,12 @@ for dir in "${changed_dirs[@]}"; do print } }' "$file" > "$tmpfile" && mv "$tmpfile" "$file" + + # Check if the README.md file has changed + if ! git diff --quiet -- "$dir/README.md"; then + echo "Bumping version in $dir/README.md from $current_tag to $LATEST_TAG (incremented)" + else + echo "Version in $dir/README.md is already up to date" + fi fi done diff --git a/vault-jwt/README.md b/vault-jwt/README.md new file mode 100644 index 0000000..67aa7e5 --- /dev/null +++ b/vault-jwt/README.md @@ -0,0 +1,77 @@ +--- +display_name: Hashicorp Vault Integration (JWT) +description: Authenticates with Vault using a JWT from Coder's OIDC provider +icon: ../.icons/vault.svg +maintainer_github: coder +partner_github: hashicorp +verified: true +tags: [helper, integration, vault, jwt, oidc] +--- + +# Hashicorp Vault Integration (JWT) + +This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/auth#openid-connect) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method. + +```tf +module "vault" { + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_role = "coder" # The Vault role to use for authentication +} +``` + +Then you can use the Vault CLI in your workspaces to fetch secrets from Vault: + +```shell +vault kv get -namespace=coder -mount=secrets coder +``` + +or using the Vault API: + +```shell +curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder" +``` + +## Examples + +### Configure Vault integration with a non standard auth path (default is "jwt") + +```tf +module "vault" { + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_auth_path = "oidc" + vault_jwt_role = "coder" # The Vault role to use for authentication +} +``` + +### Map workspace owner's group to a Vault role + +```tf +data "coder_workspace_owner" "me" {} + +module "vault" { + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_role = data.coder_workspace_owner.me.groups[0] +} +``` + +### Install a specific version of the Vault CLI + +```tf +module "vault" { + source = "registry.coder.com/modules/vault-jwt/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + vault_addr = "https://vault.example.com" + vault_jwt_role = "coder" # The Vault role to use for authentication + vault_cli_version = "1.17.5" +} +``` diff --git a/vault-jwt/main.test.ts b/vault-jwt/main.test.ts new file mode 100644 index 0000000..2fda3d7 --- /dev/null +++ b/vault-jwt/main.test.ts @@ -0,0 +1,12 @@ +import { describe } from "bun:test"; +import { runTerraformInit, testRequiredVariables } from "../test"; + +describe("vault-jwt", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + vault_addr: "foo", + vault_jwt_role: "foo", + }); +}); diff --git a/vault-jwt/main.tf b/vault-jwt/main.tf new file mode 100644 index 0000000..adcc34d --- /dev/null +++ b/vault-jwt/main.tf @@ -0,0 +1,64 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12.4" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "vault_addr" { + type = string + description = "The address of the Vault server." +} + +variable "vault_jwt_auth_path" { + type = string + description = "The path to the Vault JWT auth method." + default = "jwt" +} + +variable "vault_jwt_role" { + type = string + description = "The name of the Vault role to use for authentication." +} + +variable "vault_cli_version" { + type = string + description = "The version of Vault to install." + default = "latest" + validation { + condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.vault_cli_version)) + error_message = "Vault version must be in the format 0.0.0 or latest" + } +} + +resource "coder_script" "vault" { + agent_id = var.agent_id + display_name = "Vault (GitHub)" + icon = "/icon/vault.svg" + script = templatefile("${path.module}/run.sh", { + CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token, + VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path, + VAULT_JWT_ROLE : var.vault_jwt_role, + VAULT_CLI_VERSION : var.vault_cli_version, + }) + run_on_start = true + start_blocks_login = true +} + +resource "coder_env" "vault_addr" { + agent_id = var.agent_id + name = "VAULT_ADDR" + value = var.vault_addr +} + +data "coder_workspace_owner" "me" {} diff --git a/vault-jwt/run.sh b/vault-jwt/run.sh new file mode 100644 index 0000000..662b378 --- /dev/null +++ b/vault-jwt/run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +# Convert all templated variables to shell variables +VAULT_CLI_VERSION=${VAULT_CLI_VERSION} +VAULT_JWT_AUTH_PATH=${VAULT_JWT_AUTH_PATH} +VAULT_JWT_ROLE=${VAULT_JWT_ROLE} +CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN} + +fetch() { + dest="$1" + url="$2" + if command -v curl > /dev/null 2>&1; then + curl -sSL --fail "$${url}" -o "$${dest}" + elif command -v wget > /dev/null 2>&1; then + wget -O "$${dest}" "$${url}" + elif command -v busybox > /dev/null 2>&1; then + busybox wget -O "$${dest}" "$${url}" + else + printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n" + exit 1 + fi +} + +unzip_safe() { + if command -v unzip > /dev/null 2>&1; then + command unzip "$@" + elif command -v busybox > /dev/null 2>&1; then + busybox unzip "$@" + else + printf "unzip or busybox is not installed. Please install unzip in your image.\n" + exit 1 + fi +} + +install() { + # Get the architecture of the system + ARCH=$(uname -m) + if [ "$${ARCH}" = "x86_64" ]; then + ARCH="amd64" + elif [ "$${ARCH}" = "aarch64" ]; then + ARCH="arm64" + else + printf "Unsupported architecture: $${ARCH}\n" + return 1 + fi + # Fetch the latest version of Vault if VAULT_CLI_VERSION is 'latest' + if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then + LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1) + printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}" + if [ -z "$${LATEST_VERSION}" ]; then + printf "Failed to determine the latest Vault version.\n" + return 1 + fi + VAULT_CLI_VERSION=$${VAULT_CLI_VERSION} + fi + + # Check if the vault CLI is installed and has the correct version + installation_needed=1 + if command -v vault > /dev/null 2>&1; then + CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then + printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}" + installation_needed=0 + fi + fi + + if [ $${installation_needed} -eq 1 ]; then + # Download and install Vault + if [ -z "$${CURRENT_VERSION}" ]; then + printf "Installing Vault CLI ...\n\n" + else + printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${VAULT_CLI_VERSION}" + fi + fetch vault.zip "https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_linux_$${ARCH}.zip" + if [ $? -ne 0 ]; then + printf "Failed to download Vault.\n" + return 1 + fi + if ! unzip_safe vault.zip; then + printf "Failed to unzip Vault.\n" + return 1 + fi + rm vault.zip + if sudo mv vault /usr/local/bin/vault 2> /dev/null; then + printf "Vault installed successfully!\n\n" + else + mkdir -p ~/.local/bin + if ! mv vault ~/.local/bin/vault; then + printf "Failed to move Vault to local bin.\n" + return 1 + fi + printf "Please add ~/.local/bin to your PATH to use vault CLI.\n" + fi + fi + return 0 +} + +TMP=$(mktemp -d) +if ! ( + cd "$TMP" + install +); then + echo "Failed to install Vault CLI." + exit 1 +fi +rm -rf "$TMP" + +# Authenticate with Vault +printf "🔑 Authenticating with Vault ...\n\n" +echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- +printf "🥳 Vault authentication complete!\n\n" +printf "You can now use Vault CLI to access secrets.\n"