Add Sonatype Nexus repository integration module (#262)

# Add Sonatype Nexus Repository Integration Module

## Summary
Implements a Coder module for Sonatype Nexus Repository Manager
integration that automatically configures Maven, npm, PyPI, and Docker
registries for development workspaces.

## Demo Video & Screenshots

https://github.com/user-attachments/assets/2c51f229-d34d-483b-a0e9-f4e0d79332c2

![Nexus Repository
Integration](https://github.com/user-attachments/assets/1a778a8f-0e48-40f2-ae0f-5b8d5d5ce849)

## Features
-  **Maven Support**: Automatic `settings.xml` configuration
-  **npm Support**: Automatic `.npmrc` configuration with scoped
packages
-  **PyPI Support**: Automatic `pip.conf` configuration
-  **Docker Support**: Registry authentication setup
-  **Flexible Configuration**: Support for multiple repositories per
package manager
-  **Secure Credentials**: API token and password support
-  **Username Options**: Configurable username field (username or
email)

## Nexus Repository Manager Requirements

### Version Requirements
**Yes, this module requires Nexus Repository Manager Pro version** for
full functionality, though basic features work with the Community
Edition (OSS).

### Supported Authentication Methods
This module supports **4 authentication methods**:

1. **User Token Authentication** (Recommended - Pro only)
   - Enhanced security with two-part tokens
   - Ideal for CI/CD and automated environments
   - Requires `nx-usertoken-current` privilege

2. **API Token Authentication** (Pro only)
   - Single-use access tokens via REST API
   - Programmatic token generation and management

3. **Basic Authentication** (OSS & Pro)
   - Standard HTTP Basic Auth with username/password
   - Works with both OSS and Pro versions

4. **Base64 Encoded Credentials** (OSS & Pro)  
   - Base64 encoded `username:password` format
   - Compatible with npm and other package managers

### Testing Instructions

#### Prerequisites
- Nexus Repository Manager instance (OSS or Pro)
- Admin access to configure repositories
- Test repositories for each package manager you want to test

#### Setup Test Environment
1. **Create Test Repositories** in your Nexus instance:
   - Maven: `maven-public`, `maven-releases` 
   - npm: `npm-public`, `@company:npm-private`
   - PyPI: `pypi-public`, `pypi-private`
   - Docker: `docker-public`, `docker-private`

2. **Configure Authentication**:
   - For Pro: Generate user tokens via UI (User menu → User Token)
   - For OSS: Use username/password or base64 encoded credentials
   - Set up appropriate permissions for test repositories

3. **Test the Module**:
   ```hcl
   module "nexus" {
     source         = "registry.coder.com/mavrickrishi/nexus/coder"
     version        = "1.0.0"
     agent_id       = coder_agent.main.id
     nexus_url      = "https://your-nexus-instance.com"
     nexus_password = var.nexus_api_token  # or password
     package_managers = {
       maven  = ["maven-public", "maven-releases"]
       npm    = ["npm-public", "@company:npm-private"]
       pypi   = ["pypi-public", "pypi-private"]
       docker = ["docker-public", "docker-private"]
     }
   }
   ```

4. **Verify Configuration**:
   - Check generated config files in workspace
   - Test package installation from configured repositories
   - Verify authentication works for each package manager

#### EC2 Deployment Testing
Tested by deploying on EC2 instance with:
- Ubuntu 22.04 LTS
- Nexus Repository Manager Pro
- All package managers (Maven, npm, PyPI, Docker)
- Both token and basic authentication methods

## Usage Example
```hcl
module "nexus" {
  source         = "registry.coder.com/mavrickrishi/nexus/coder"
  version        = "1.0.0"
  agent_id       = coder_agent.main.id
  nexus_url      = "https://nexus.company.com"
  nexus_password = var.nexus_api_token
  package_managers = {
    maven  = ["maven-public", "maven-releases"]
    npm    = ["npm-public", "@company:npm-private"]
    pypi   = ["pypi-public", "pypi-private"]
    docker = ["docker-public", "docker-private"]
  }
}
```

## Testing
-  11 comprehensive tests covering all functionality
-  Variable validation tests
-  Package manager configuration tests
-  Error handling tests
-  All tests passing
-  EC2 deployment tested

## Files Added
- `registry/mavrickrishi/modules/nexus/main.tf` - Main module
configuration
- `registry/mavrickrishi/modules/nexus/README.md` - Complete
documentation
- `registry/mavrickrishi/modules/nexus/main.test.ts` - Test suite

## Checklist
- [x] Module follows existing patterns and conventions
- [x] Comprehensive test coverage (11 tests)
- [x] Complete documentation with examples
- [x] Input validation and error handling
- [x] Secure credential handling
- [x] All tests passing
- [x] Demo video included
- [x] Screenshots added
- [x] Testing instructions provided
- [x] Authentication methods documented
- [x] EC2 deployment tested

Closes #202
/claim #202

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <me@matifali.dev>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
This commit is contained in:
Rishi Mondal
2025-10-09 18:01:43 +05:30
committed by GitHub
parent 8acda84dd7
commit ce039f64df
6 changed files with 544 additions and 0 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

+1
View File
@@ -19,4 +19,5 @@ participating in LFX CNCF programs, and helping the developer community grow.
## Modules
- **aws-ami-snapshot**: Create and manage AMI snapshots for Coder workspaces with restore capabilities
- [nexus-repository](./modules/nexus-repository/) - Configure package managers to use Sonatype Nexus Repository
- [auto-start-dev-server](modules/auto-start-dev-server/README.md) - Automatically detect and start development servers for various project types
@@ -0,0 +1,149 @@
---
display_name: 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"]
}
}
```
@@ -0,0 +1,147 @@
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();
});
});
@@ -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}"
])
}
@@ -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'
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<servers>
<server>
<id>nexus</id>
<username>${NEXUS_USERNAME}</username>
<password>${NEXUS_PASSWORD}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>nexus-mirror</id>
<mirrorOf>*</mirrorOf>
<url>${NEXUS_URL}/repository/${MAVEN_REPO}</url>
</mirror>
</mirrors>
</settings>
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!"