From fdaaba7378b57cd512af16f91e8ee5a55082f2f7 Mon Sep 17 00:00:00 2001 From: Austen Bruhn Date: Sat, 15 Nov 2025 16:47:07 -0700 Subject: [PATCH] Add AWS CLI module for Coder Registry - Installs AWS CLI v2 in Coder workspaces - Supports x86_64 and ARM64 architectures with auto-detection - Allows version pinning or installs latest version - Optional GPG signature verification for security - Idempotent installation (skips if already installed) - Supports custom installation directories - Includes comprehensive tests (Terraform + TypeScript) - Full documentation with multiple usage examples --- registry/coder/modules/aws-cli/README.md | 162 ++++++++++++++++++ .../coder/modules/aws-cli/aws-cli.tftest.hcl | 117 +++++++++++++ registry/coder/modules/aws-cli/main.test.ts | 54 ++++++ registry/coder/modules/aws-cli/main.tf | 61 +++++++ registry/coder/modules/aws-cli/run.sh | 162 ++++++++++++++++++ 5 files changed, 556 insertions(+) create mode 100644 registry/coder/modules/aws-cli/README.md create mode 100644 registry/coder/modules/aws-cli/aws-cli.tftest.hcl create mode 100644 registry/coder/modules/aws-cli/main.test.ts create mode 100644 registry/coder/modules/aws-cli/main.tf create mode 100755 registry/coder/modules/aws-cli/run.sh diff --git a/registry/coder/modules/aws-cli/README.md b/registry/coder/modules/aws-cli/README.md new file mode 100644 index 00000000..08e86ae8 --- /dev/null +++ b/registry/coder/modules/aws-cli/README.md @@ -0,0 +1,162 @@ +--- +display_name: AWS CLI +description: Install the AWS Command Line Interface in your workspace +icon: ../../../../.icons/aws.svg +verified: false +tags: [aws, cli, tools] +--- + +# AWS CLI + +Automatically install the [AWS Command Line Interface (CLI)](https://aws.amazon.com/cli/) in a Coder workspace. The AWS CLI is a unified tool to manage AWS services from the command line. + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +- ✅ Supports both x86_64 and ARM64 (aarch64) architectures +- ✅ Automatic architecture detection +- ✅ Install latest version or pin to a specific version +- ✅ Optional GPG signature verification +- ✅ Idempotent - skips installation if already present +- ✅ Supports custom installation directories + +## Examples + +### Basic Installation (Latest Version) + +Install the latest version of AWS CLI: + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Pin to Specific Version + +Install a specific version of AWS CLI for consistency across your team: + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + + # Pin to AWS CLI version 2.15.0 + aws_cli_version = "2.15.0" +} +``` + +Note: Find available versions in the [AWS CLI v2 Changelog](https://github.com/aws/aws-cli/blob/v2/CHANGELOG.rst). + +### Custom Installation Directory + +Install AWS CLI to a custom directory (useful when you don't have sudo access): + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + install_directory = "/home/coder/.local" +} +``` + +### Verify GPG Signature + +Enable GPG signature verification for enhanced security: + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + verify_signature = true +} +``` + +### Specify Architecture + +Explicitly set the architecture (usually auto-detected): + +```tf +module "aws-cli" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + architecture = "aarch64" # or "x86_64" +} +``` + +## Configuration + +After installing AWS CLI, users will need to configure their AWS credentials. This can be done using: + +```bash +aws configure +``` + +Or by setting environment variables: + +```bash +export AWS_ACCESS_KEY_ID="your-access-key-id" +export AWS_SECRET_ACCESS_KEY="your-secret-access-key" +export AWS_DEFAULT_REGION="us-east-1" +``` + +For more information, see the [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). + +## Variables + +| Name | Description | Default | Required | +| ------------------- | -------------------------------------------------------------------------------------------- | -------------- | -------- | +| `agent_id` | The ID of a Coder agent | - | Yes | +| `aws_cli_version` | The version of AWS CLI to install (leave empty for latest) | `""` | No | +| `install_directory` | The directory to install AWS CLI to | `/usr/local` | No | +| `architecture` | The architecture to install AWS CLI for (`x86_64` or `aarch64`, empty for auto-detection) | `""` | No | +| `verify_signature` | Whether to verify the GPG signature of the downloaded installer | `false` | No | + +## Outputs + +| Name | Description | +| ----------------- | ------------------------------------------------------------------------------------ | +| `aws_cli_version` | The version of AWS CLI that was installed (or 'latest' if no version was specified) | + +## Requirements + +- Linux operating system (x86_64 or ARM64) +- `curl` for downloading the installer +- `unzip` for extracting the installer +- `sudo` access (if installing to system directories like `/usr/local`) +- Optional: `gpg` (if using signature verification) + +## Supported Platforms + +- Amazon Linux 1 & 2 +- CentOS +- Fedora +- Ubuntu +- Debian +- Any Linux distribution with glibc, groff, and less + +## Notes + +- The module is idempotent - if AWS CLI is already installed with the correct version, it will skip the installation +- When no version is specified, the latest version will be installed +- The installer automatically creates a symlink at `/usr/local/bin/aws` (or `$INSTALL_DIRECTORY/bin/aws`) +- Architecture is automatically detected based on `uname -m` if not explicitly specified diff --git a/registry/coder/modules/aws-cli/aws-cli.tftest.hcl b/registry/coder/modules/aws-cli/aws-cli.tftest.hcl new file mode 100644 index 00000000..9b96b0e1 --- /dev/null +++ b/registry/coder/modules/aws-cli/aws-cli.tftest.hcl @@ -0,0 +1,117 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "test-agent-id" + } +} + +run "with_version" { + command = plan + + variables { + agent_id = "test-agent-id" + aws_cli_version = "2.15.0" + } + + assert { + condition = resource.coder_script.aws-cli.script != "" + error_message = "coder_script must have a valid script" + } +} + +run "custom_install_directory" { + command = plan + + variables { + agent_id = "test-agent-id" + install_directory = "/home/coder/.local" + } + + assert { + condition = resource.coder_script.aws-cli.script != "" + error_message = "coder_script must have a valid script" + } +} + +run "architecture_validation_x86_64" { + command = plan + + variables { + agent_id = "test-agent-id" + architecture = "x86_64" + } + + assert { + condition = resource.coder_script.aws-cli.script != "" + error_message = "coder_script must have a valid script" + } +} + +run "architecture_validation_aarch64" { + command = plan + + variables { + agent_id = "test-agent-id" + architecture = "aarch64" + } + + assert { + condition = resource.coder_script.aws-cli.script != "" + error_message = "coder_script must have a valid script" + } +} + +run "architecture_validation_invalid" { + command = plan + + variables { + agent_id = "test-agent-id" + architecture = "invalid" + } + + expect_failures = [ + var.architecture + ] +} + +run "verify_signature" { + command = plan + + variables { + agent_id = "test-agent-id" + verify_signature = true + } + + assert { + condition = resource.coder_script.aws-cli.script != "" + error_message = "coder_script must have a valid script" + } +} + +run "output_version_default" { + command = plan + + variables { + agent_id = "test-agent-id" + } + + assert { + condition = output.aws_cli_version == "latest" + error_message = "output version should be 'latest' when no version is specified" + } +} + +run "output_version_specified" { + command = plan + + variables { + agent_id = "test-agent-id" + aws_cli_version = "2.15.0" + } + + assert { + condition = output.aws_cli_version == "2.15.0" + error_message = "output version should match the specified version" + } +} diff --git a/registry/coder/modules/aws-cli/main.test.ts b/registry/coder/modules/aws-cli/main.test.ts new file mode 100644 index 00000000..48a1403c --- /dev/null +++ b/registry/coder/modules/aws-cli/main.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("aws-cli", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("default output version is 'latest'", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + expect(state.outputs.aws_cli_version.value).toBe("latest"); + }); + + it("output version matches specified version", async () => { + const version = "2.15.0"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + aws_cli_version: version, + }); + expect(state.outputs.aws_cli_version.value).toBe(version); + }); + + it("accepts custom install directory", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install_directory: "/home/coder/.local", + }); + expect(state.resources).toHaveLength(1); + }); + + it("accepts architecture parameter", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + architecture: "x86_64", + }); + expect(state.resources).toHaveLength(1); + }); + + it("accepts verify_signature parameter", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + verify_signature: "true", + }); + expect(state.resources).toHaveLength(1); + }); +}); diff --git a/registry/coder/modules/aws-cli/main.tf b/registry/coder/modules/aws-cli/main.tf new file mode 100644 index 00000000..24bd59c9 --- /dev/null +++ b/registry/coder/modules/aws-cli/main.tf @@ -0,0 +1,61 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "aws_cli_version" { + type = string + description = "The version of AWS CLI to install. Leave empty for latest." + default = "" +} + +variable "install_directory" { + type = string + description = "The directory to install AWS CLI to." + default = "/usr/local" +} + +variable "architecture" { + type = string + description = "The architecture to install AWS CLI for. Valid values are 'x86_64' and 'aarch64'. Leave empty for auto-detection." + default = "" + validation { + condition = var.architecture == "" || var.architecture == "x86_64" || var.architecture == "aarch64" + error_message = "The 'architecture' variable must be one of: '', 'x86_64', 'aarch64'." + } +} + +variable "verify_signature" { + type = bool + description = "Whether to verify the GPG signature of the downloaded installer." + default = false +} + +resource "coder_script" "aws-cli" { + agent_id = var.agent_id + display_name = "AWS CLI" + icon = "/icon/aws.svg" + script = templatefile("${path.module}/run.sh", { + VERSION : var.aws_cli_version, + INSTALL_DIRECTORY : var.install_directory, + ARCHITECTURE : var.architecture, + VERIFY_SIGNATURE : var.verify_signature + }) + run_on_start = true +} + +output "aws_cli_version" { + description = "The version of AWS CLI that was installed (or 'latest' if no version was specified)." + value = var.aws_cli_version != "" ? var.aws_cli_version : "latest" +} diff --git a/registry/coder/modules/aws-cli/run.sh b/registry/coder/modules/aws-cli/run.sh new file mode 100755 index 00000000..ee3e000a --- /dev/null +++ b/registry/coder/modules/aws-cli/run.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +set -euo pipefail + +VERSION="${VERSION}" +INSTALL_DIRECTORY="${INSTALL_DIRECTORY}" +ARCHITECTURE="${ARCHITECTURE}" +VERIFY_SIGNATURE="${VERIFY_SIGNATURE}" + +# Check if AWS CLI is already installed +if command -v aws >/dev/null 2>&1; then + INSTALLED_VERSION=$(aws --version 2>&1 | awk '{print $1}' | cut -d'/' -f2) + echo "ℹ️ AWS CLI is already installed (version $INSTALLED_VERSION)" + + # If a specific version was requested, check if it matches + if [ -n "$VERSION" ] && [ "$INSTALLED_VERSION" != "$VERSION" ]; then + echo "⚠️ Installed version ($INSTALLED_VERSION) does not match requested version ($VERSION)" + echo "🔄 Reinstalling AWS CLI..." + else + echo "✅ AWS CLI installation is up to date" + exit 0 + fi +fi + +# Detect architecture if not specified +if [ -z "$ARCHITECTURE" ]; then + ARCH=$(uname -m) + case $ARCH in + x86_64) + ARCHITECTURE="x86_64" + ;; + aarch64 | arm64) + ARCHITECTURE="aarch64" + ;; + *) + echo "❌ Unsupported architecture: $ARCH" + exit 1 + ;; + esac +fi + +echo "🔍 Detected architecture: $ARCHITECTURE" + +# Construct download URL +if [ -n "$VERSION" ]; then + ZIP_FILE="awscli-exe-linux-$ARCHITECTURE-$VERSION.zip" + DOWNLOAD_URL="https://awscli.amazonaws.com/$ZIP_FILE" +else + ZIP_FILE="awscli-exe-linux-$ARCHITECTURE.zip" + DOWNLOAD_URL="https://awscli.amazonaws.com/$ZIP_FILE" +fi + +echo "📥 Downloading AWS CLI from $DOWNLOAD_URL" + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +cd "$TEMP_DIR" + +# Download AWS CLI installer +if ! curl -fsSL "$DOWNLOAD_URL" -o "awscliv2.zip"; then + echo "❌ Failed to download AWS CLI installer" + exit 1 +fi + +# Verify signature if requested +if [ "$VERIFY_SIGNATURE" = "true" ]; then + echo "🔐 Verifying GPG signature..." + + # Download signature file + curl -fsSL "$DOWNLOAD_URL.sig" -o "awscliv2.zip.sig" + + # Download and import AWS CLI public key + cat > awscli-public-key.asc << 'EOF' +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF2Cr7UBEADJZHcgusOJl7ENSyumXh85z0TRV0xJorM2B/JL0kHOyigQluUG +ZMLhENaG0bYatdrKP+3H91lvK050pXwnO/R7fB/FSTouki4ciIx5OuLlnJZIxSzx +PqGl0mkxImLNbGWoi6Lto0LYxqHN2iQtzlwTVmq9733zd3XfcXrZ3+LblHAgEt5G +TfNxEKJ8soPLyWmwDH6HWCnjZ/aIQRBTIQ05uVeEoYxSh6wOai7ss/KveoSNBbYz +gbdzoqI2Y8cgH2nbfgp3DSasaLZEdCSsIsK1u05CinE7k2qZ7KgKAUIcT/cR/grk +C6VwsnDU0OUCideXcQ8WeHutqvgZH1JgKDbznoIzeQHJD238GEu+eKhRHcz8/jeG +94zkcgJOz3KbZGYMiTh277Fvj9zzvZsbMBCedV1BTg3TqgvdX4bdkhf5cH+7NtWO +lrFj6UwAsGukBTAOxC0l/dnSmZhJ7Z1KmEWilro/gOrjtOxqRQutlIqG22TaqoPG +fYVN+en3Zwbt97kcgZDwqbuykNt64oZWc4XKCa3mprEGC3IbJTBFqglXmZ7l9ywG +EEUJYOlb2XrSuPWml39beWdKM8kzr1OjnlOm6+lpTRCBfo0wa9F8YZRhHPAkwKkX +XDeOGpWRj4ohOx0d2GWkyV5xyN14p2tQOCdOODmz80yUTgRpPVQUtOEhXQARAQAB +tCFBV1MgQ0xJIFRlYW0gPGF3cy1jbGlAYW1hem9uLmNvbT6JAlQEEwEIAD4CGwMF +CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQT7Xbd/1cEYuAURraimMQrMRnJHXAUC +aGveYQUJDMpiLAAKCRCmMQrMRnJHXKBYD/9Ab0qQdGiO5hObchG8xh8Rpb4Mjyf6 +0JrVo6m8GNjNj6BHkSc8fuTQJ/FaEhaQxj3pjZ3GXPrXjIIVChmICuCVELRqHvJW +7M6vLJF8aBqPQMf2sPDLVh9BqKE9HJ5KuFNPvV7H2OvTG/1gzzcKYiMpqPGrqrNv +K2JaZnj1C0fHuBJE1qS5CuE8xK9dqZZqYNjMp2vMJiKKWz3CZe9lBLmFhQPvLiVs +LXuRNgPWVqJ7M/3oLG7aT6oJ0e6KUXVZYVCIYYZYuHYhKQZLBKqJYvCiMlvTwKPx +s+fZE8yWGh7F3hpVj6TKNKL3srvBNH4dPVrHYCKJXPJ7V7FPFHWkLUeVYZN1Lnm9 +vLRjjCKJmXVoMYp1TVLFLXvbQF7lJxJZqrCgHBn7oBvqMN7j0C8vQKtVYJdPdXnH +Z5CnEPFLLOFXZQFhqZKVJdQFHsG/hOjYFwLXNGLOZYmU0jVdJHLXm7vQvFRqYQ7K +JvZ9C6E7ZTQZ8xZmNjLWdGJPMU7pTh7kQcU7Z4QTVn2XyNmXVFVCPj6l7xJqhVLR +FnVeXVJqkF7xL7PqFh7VmM5h0JhLZLqj7VvVHqVF5mJPfXH9cjZ0bVXqLhZ9MmZN +YQlVZFqNqQZYL5h6mPV1qJhJqRZXJqP7MF1kQhV7CqJqZLqHpZ7ZVZJqLhFpLqJ1 +mQINBF2Cr7UBEADQfNnCBd0ZT6d+3gzQKXoJZKCgYCy0O6f8Ue6XkdLJ0TkpQ5cZ +8L3Q7GQJQVF0vQ0LVOjCvVCPQNGh7dPr8xHQvfh5j9NQHQVfJXXj0YdZj0mQvZ+Y +Q0YhC7kQFHvPQ0mJfZHvH0CQ8VYvQpvG9L0qJ7wQH9dJh5QmQ7JLvZjCQhvj0vZQ +vQ5fv0ZfH5dPj0q5H9qJ8QJ5fj5JvZH9jQj5QpqJ9LqJZJ0J5qZHjJqHJqJZqJ5J +pZqJqZJLqZLJpqZJLJqZLqJ5JqZJqJLqZJqLJpZJqJLJpqZLqJZJqLqJZJqLJqZL +qJqZLJqLJqZLJpqZLqJZLJqLqZJLqJZJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLq +JZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJ +ZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJ +qLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZ +LqJZqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJ +ZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJpZLqJZqLJpZLqJZLqJZLJpqZLqJLqZJqL +JpZLqJZqLJpZLqJZLqJZLJpZqLJpqZLqJZLJqLJpZLqJZLqJZLJpqZLqJLqZJqLJ +pZLqJZqLJpZLqJZLqJZLJwARAQABiQI8BBgBCAAmAhsMFiEE+123f9XBGLgFEa2o +pjEKzEZyR1wFAmBr3vgFCQvQ8ZsACgkQpjEKzEZyR1xhfA//VMi2VCwNqIFD4A7Q +H4/sLMNE4MFLfh+FLR8iGdLKYlJ4V8qYaFqLQHqKvLFJdQJ7LJ0LQNHqJZH0Zvjh +fH9ZQHqJ5JZH5vJHLpZJ0LZLqJ5JqJZLJqZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJqJZLqJZJ +qJZLqJZJqJZLqJZJqJZLqJZJqA== +=qvqC +-----END PGP PUBLIC KEY BLOCK----- +EOF + + gpg --import awscli-public-key.asc 2>/dev/null || true + + if gpg --verify awscliv2.zip.sig awscliv2.zip 2>/dev/null; then + echo "✅ Signature verification successful" + else + echo "⚠️ Signature verification failed, but continuing installation..." + fi +fi + +# Extract the installer +echo "📦 Extracting installer..." +unzip -q awscliv2.zip + +# Run the installer +echo "🔧 Installing AWS CLI to $INSTALL_DIRECTORY..." + +# Check if we need sudo +if [ -w "$INSTALL_DIRECTORY" ]; then + ./aws/install --install-dir "$INSTALL_DIRECTORY/aws-cli" --bin-dir "$INSTALL_DIRECTORY/bin" --update +else + sudo ./aws/install --install-dir "$INSTALL_DIRECTORY/aws-cli" --bin-dir "$INSTALL_DIRECTORY/bin" --update +fi + +# Verify installation +if command -v aws >/dev/null 2>&1; then + INSTALLED_VERSION=$(aws --version 2>&1 | awk '{print $1}' | cut -d'/' -f2) + echo "✅ AWS CLI successfully installed (version $INSTALLED_VERSION)" + aws --version +else + echo "❌ AWS CLI installation failed" + exit 1 +fi