diff --git a/.github/typos.toml b/.github/typos.toml index f27257a23..7d53f9bf1 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -2,6 +2,8 @@ muc = "muc" # For Munich location code Hashi = "Hashi" HashiCorp = "HashiCorp" +mavrickrishi = "mavrickrishi" # Username +mavrick = "mavrick" # Username [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/.icons/nexus-repository.svg b/.icons/nexus-repository.svg new file mode 100644 index 000000000..42875c847 --- /dev/null +++ b/.icons/nexus-repository.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/mavrickrishi/README.md b/registry/mavrickrishi/README.md new file mode 100644 index 000000000..c240f3bcc --- /dev/null +++ b/registry/mavrickrishi/README.md @@ -0,0 +1,14 @@ +--- +display_name: mavrickrishi +bio: Coder module contributor +github: mavrick-1 +status: community +--- + +# mavrickrishi + +This directory contains Coder modules and templates created by mavrickrishi. + +## Modules + +- [nexus-repository](./modules/nexus-repository/) - Configure package managers to use Sonatype Nexus Repository diff --git a/registry/mavrickrishi/modules/nexus-repository/README.md b/registry/mavrickrishi/modules/nexus-repository/README.md new file mode 100644 index 000000000..6c2c1926c --- /dev/null +++ b/registry/mavrickrishi/modules/nexus-repository/README.md @@ -0,0 +1,149 @@ +--- +display_name: Sonatype Nexus Repository +description: Configure package managers to use Sonatype Nexus Repository for Maven, npm, PyPI, and Docker registries. +icon: ../../../../.icons/nexus-repository.svg +verified: true +tags: [integration, nexus-repository, maven, npm, pypi, docker] +--- + +# Sonatype Nexus Repository + +Configure package managers (Maven, npm, Go, PyPI, Docker) to use [Sonatype Nexus Repository](https://help.sonatype.com/en/sonatype-nexus-repository.html) with API token authentication. This module provides secure credential handling, multiple repository support per package manager, and flexible username configuration. + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + maven = ["maven-public", "maven-releases"] + npm = ["npm-public", "@scoped:npm-private"] + go = ["go-public", "go-private"] + pypi = ["pypi-public", "pypi-private"] + docker = ["docker-public", "docker-private"] + } +} +``` + +## Requirements + +- Nexus Repository Manager 3.x +- Valid API token or user credentials +- Package managers installed on the workspace (Maven, npm, Go, pip, Docker as needed) + +> [!NOTE] +> This module configures package managers but does not install them. You need to handle the installation of Maven, npm, Go, Python pip, and Docker yourself. + +## Examples + +### Configure Maven to use Nexus repositories + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + maven = ["maven-public", "maven-releases", "maven-snapshots"] + } +} +``` + +### Configure npm with scoped packages + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + npm = ["npm-public", "@mycompany:npm-private"] + } +} +``` + +### Configure Go module proxy + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + go = ["go-public", "go-private"] + } +} +``` + +### Configure Python PyPI repositories + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + pypi = ["pypi-public", "pypi-private"] + } +} +``` + +### Configure Docker registries + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + docker = ["docker-public", "docker-private"] + } +} +``` + +### Use custom username + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_username = "custom-user" + nexus_password = var.nexus_api_token + package_managers = { + maven = ["maven-public"] + } +} +``` + +### Complete configuration for all package managers + +```tf +module "nexus_repository" { + source = "registry.coder.com/mavrickrishi/nexus-repository/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + nexus_url = "https://nexus.example.com" + nexus_password = var.nexus_api_token + package_managers = { + maven = ["maven-public", "maven-releases"] + npm = ["npm-public", "@company:npm-private"] + go = ["go-public", "go-private"] + pypi = ["pypi-public", "pypi-private"] + docker = ["docker-public", "docker-private"] + } +} +``` diff --git a/registry/mavrickrishi/modules/nexus-repository/main.test.ts b/registry/mavrickrishi/modules/nexus-repository/main.test.ts new file mode 100644 index 000000000..a9e7b484d --- /dev/null +++ b/registry/mavrickrishi/modules/nexus-repository/main.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("nexus-repository", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-password" + }); + + it("configures Maven settings", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({ + maven: ["maven-public"] + }) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("☕ Configuring Maven..."); + expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!"); + }); + + it("configures npm registry", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({ + npm: ["npm-public"] + }) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("📦 Configuring npm..."); + expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!"); + }); + + it("configures PyPI repository", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({ + pypi: ["pypi-public"] + }) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("🐍 Configuring pip..."); + expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!"); + }); + + it("configures multiple package managers", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({ + maven: ["maven-public"], + npm: ["npm-public"], + pypi: ["pypi-public"] + }) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("☕ Configuring Maven..."); + expect(output.stdout.join("\n")).toContain("📦 Configuring npm..."); + expect(output.stdout.join("\n")).toContain("🐍 Configuring pip..."); + expect(output.stdout.join("\n")).toContain("✅ Nexus repository configuration completed!"); + }); + + it("handles empty package managers", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({}) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("🤔 no maven repository is set, skipping maven configuration."); + expect(output.stdout.join("\n")).toContain("🤔 no npm repository is set, skipping npm configuration."); + expect(output.stdout.join("\n")).toContain("🤔 no pypi repository is set, skipping pypi configuration."); + expect(output.stdout.join("\n")).toContain("🤔 no docker repository is set, skipping docker configuration."); + }); + + it("configures Go module proxy", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + package_managers: JSON.stringify({ + go: ["go-public", "go-private"] + }) + }); + + const output = await executeScriptInContainer(state, "ubuntu:20.04"); + expect(output.stdout.join("\n")).toContain("🐹 Configuring Go..."); + expect(output.stdout.join("\n")).toContain("Go proxy configured via GOPROXY environment variable"); + expect(output.stdout.join("\n")).toContain("🥳 Configuration complete!"); + }); + + it("validates nexus_url format", async () => { + await expect( + runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "invalid-url", + nexus_password: "test-token", + package_managers: JSON.stringify({}) + }) + ).rejects.toThrow(); + }); + + it("validates username_field values", async () => { + await expect( + runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + nexus_url: "https://nexus.example.com", + nexus_password: "test-token", + username_field: "invalid", + package_managers: JSON.stringify({}) + }) + ).rejects.toThrow(); + }); +}); \ No newline at end of file diff --git a/registry/mavrickrishi/modules/nexus-repository/main.tf b/registry/mavrickrishi/modules/nexus-repository/main.tf new file mode 100644 index 000000000..be573121f --- /dev/null +++ b/registry/mavrickrishi/modules/nexus-repository/main.tf @@ -0,0 +1,137 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "nexus_url" { + type = string + description = "The base URL of your Nexus repository manager (e.g. https://nexus.example.com)" + validation { + condition = can(regex("^(https|http)://", var.nexus_url)) + error_message = "nexus_url must be a valid URL starting with either 'https://' or 'http://'" + } +} + +variable "nexus_username" { + type = string + description = "Custom username for Nexus authentication. If not provided, defaults to the Coder username based on the username_field setting" + default = null +} + +variable "nexus_password" { + type = string + description = "API token or password for Nexus authentication. This value is sensitive and should be stored securely" + sensitive = true +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "package_managers" { + type = object({ + maven = optional(list(string), []) + npm = optional(list(string), []) + go = optional(list(string), []) + pypi = optional(list(string), []) + docker = optional(list(string), []) + }) + default = { + maven = [] + npm = [] + go = [] + pypi = [] + docker = [] + } + description = <<-EOF + Configuration for package managers. Each key maps to a list of Nexus repository names: + - maven: List of Maven repository names + - npm: List of npm repository names (supports scoped packages with "@scope:repo-name") + - go: List of Go proxy repository names + - pypi: List of PyPI repository names + - docker: List of Docker registry names + Unused package managers can be omitted. + Example: + { + maven = ["maven-public", "maven-releases"] + npm = ["npm-public", "@scoped:npm-private"] + go = ["go-public", "go-private"] + pypi = ["pypi-public", "pypi-private"] + docker = ["docker-public", "docker-private"] + } + EOF +} + +variable "username_field" { + type = string + description = "Field to use for username (\"username\" or \"email\"). Defaults to \"username\". Only used when nexus_username is not provided" + default = "username" + validation { + condition = can(regex("^(email|username)$", var.username_field)) + error_message = "username_field must be either 'email' or 'username'" + } +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + username = coalesce(var.nexus_username, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name) + nexus_host = split("/", replace(replace(var.nexus_url, "https://", ""), "http://", ""))[0] +} + +locals { + # Get first repository name or use default + maven_repo = length(var.package_managers.maven) > 0 ? var.package_managers.maven[0] : "maven-public" + npm_repo = length(var.package_managers.npm) > 0 ? var.package_managers.npm[0] : "npm-public" + go_repo = length(var.package_managers.go) > 0 ? var.package_managers.go[0] : "go-public" + pypi_repo = length(var.package_managers.pypi) > 0 ? var.package_managers.pypi[0] : "pypi-public" + + npmrc = <<-EOF +registry=${var.nexus_url}/repository/${local.npm_repo}/ +//${local.nexus_host}/repository/${local.npm_repo}/:username=${local.username} +//${local.nexus_host}/repository/${local.npm_repo}/:_password=${base64encode(var.nexus_password)} +//${local.nexus_host}/repository/${local.npm_repo}/:always-auth=true +EOF +} + +resource "coder_script" "nexus" { + agent_id = var.agent_id + display_name = "nexus-repository" + icon = "/icon/nexus-repository.svg" + script = templatefile("${path.module}/run.sh", { + NEXUS_URL = var.nexus_url + NEXUS_HOST = local.nexus_host + NEXUS_USERNAME = local.username + NEXUS_PASSWORD = var.nexus_password + HAS_MAVEN = length(var.package_managers.maven) == 0 ? "" : "YES" + MAVEN_REPO = local.maven_repo + HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES" + NPMRC = local.npmrc + HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES" + GO_REPO = local.go_repo + HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES" + PYPI_REPO = local.pypi_repo + HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES" + REGISTER_DOCKER = join("\n ", formatlist("register_docker \"%s\"", var.package_managers.docker)) + }) + run_on_start = true +} + +resource "coder_env" "goproxy" { + count = length(var.package_managers.go) == 0 ? 0 : 1 + agent_id = var.agent_id + name = "GOPROXY" + value = join(",", [ + for repo in var.package_managers.go : + "https://${local.username}:${var.nexus_password}@${local.nexus_host}/repository/${repo}" + ]) +} + diff --git a/registry/mavrickrishi/modules/nexus-repository/run.sh b/registry/mavrickrishi/modules/nexus-repository/run.sh new file mode 100644 index 000000000..3539374b1 --- /dev/null +++ b/registry/mavrickrishi/modules/nexus-repository/run.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +not_configured() { + type=$1 + echo "🤔 no $type repository is set, skipping $type configuration." +} + +config_complete() { + echo "🥳 Configuration complete!" +} + +register_docker() { + repo=$1 + echo -n "${NEXUS_PASSWORD}" | docker login "${NEXUS_HOST}/repository/$${repo}" --username "${NEXUS_USERNAME}" --password-stdin +} + +echo "🚀 Configuring Nexus repository access..." + +# Configure Maven +if [ -n "${HAS_MAVEN}" ]; then + echo "☕ Configuring Maven..." + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml << 'EOF' + + + + + nexus + ${NEXUS_USERNAME} + ${NEXUS_PASSWORD} + + + + + nexus-mirror + * + ${NEXUS_URL}/repository/${MAVEN_REPO} + + + +EOF + config_complete +else + not_configured maven +fi + +# Configure npm +if [ -n "${HAS_NPM}" ]; then + echo "📦 Configuring npm..." + cat > ~/.npmrc << 'EOF' +${NPMRC} +EOF + config_complete +else + not_configured npm +fi + +# Configure Go +if [ -n "${HAS_GO}" ]; then + echo "🐹 Configuring Go..." + # Go configuration is handled via GOPROXY environment variable + # which is set by the Terraform configuration + echo "Go proxy configured via GOPROXY environment variable" + config_complete +else + not_configured go +fi + +# Configure pip +if [ -n "${HAS_PYPI}" ]; then + echo "🐍 Configuring pip..." + mkdir -p ~/.pip + # Create .netrc file for secure credential storage + cat > ~/.netrc << EOF +machine ${NEXUS_HOST} +login ${NEXUS_USERNAME} +password ${NEXUS_PASSWORD} +EOF + chmod 600 ~/.netrc + + # Update pip.conf to use index-url without embedded credentials + cat > ~/.pip/pip.conf << 'EOF' +[global] +index-url = https://${NEXUS_HOST}/repository/${PYPI_REPO}/simple +EOF + config_complete +else + not_configured pypi +fi + +# Configure Docker +if [ -n "${HAS_DOCKER}" ]; then + if command -v docker > /dev/null 2>&1; then + echo "🐳 Configuring Docker credentials..." + mkdir -p ~/.docker + ${REGISTER_DOCKER} + config_complete + else + echo "🤔 Docker is not installed, skipping Docker configuration." + fi +else + not_configured docker +fi + +echo "✅ Nexus repository configuration completed!" \ No newline at end of file