diff --git a/.gitignore b/.gitignore index 55947fc5..8d1b05c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store # Logs logs *.log diff --git a/.icons/scaleway.svg b/.icons/scaleway.svg new file mode 100644 index 00000000..ebe1ddf2 --- /dev/null +++ b/.icons/scaleway.svg @@ -0,0 +1,2 @@ + +Scaleway icon \ No newline at end of file diff --git a/registry/anomaly/modules/tmux/README.md b/registry/anomaly/modules/tmux/README.md index d5f22ff5..7c7af00e 100644 --- a/registry/anomaly/modules/tmux/README.md +++ b/registry/anomaly/modules/tmux/README.md @@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities ```tf module "tmux" { source = "registry.coder.com/anomaly/tmux/coder" - version = "1.0.3" + version = "1.0.4" agent_id = coder_agent.example.id } ``` @@ -39,7 +39,7 @@ module "tmux" { ```tf module "tmux" { source = "registry.coder.com/anomaly/tmux/coder" - version = "1.0.3" + version = "1.0.4" agent_id = coder_agent.example.id tmux_config = "" # Optional: custom tmux.conf content save_interval = 1 # Optional: save interval in minutes @@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the ```tf module "tmux" { source = "registry.coder.com/anomaly/tmux/coder" - version = "1.0.3" + version = "1.0.4" agent_id = var.agent_id sessions = ["default", "dev", "anomaly"] tmux_config = <<-EOT diff --git a/registry/anomaly/modules/tmux/main.tf b/registry/anomaly/modules/tmux/main.tf index 36f8471f..ef58f391 100644 --- a/registry/anomaly/modules/tmux/main.tf +++ b/registry/anomaly/modules/tmux/main.tf @@ -55,7 +55,7 @@ resource "coder_script" "tmux" { display_name = "tmux" icon = "/icon/terminal.svg" script = templatefile("${path.module}/scripts/run.sh", { - TMUX_CONFIG = var.tmux_config + TMUX_CONFIG = base64encode(var.tmux_config) SAVE_INTERVAL = var.save_interval }) run_on_start = true diff --git a/registry/anomaly/modules/tmux/scripts/run.sh b/registry/anomaly/modules/tmux/scripts/run.sh index b3c518c5..06c114d2 100755 --- a/registry/anomaly/modules/tmux/scripts/run.sh +++ b/registry/anomaly/modules/tmux/scripts/run.sh @@ -4,7 +4,7 @@ BOLD='\033[0;1m' # Convert templated variables to shell variables SAVE_INTERVAL="${SAVE_INTERVAL}" -TMUX_CONFIG="${TMUX_CONFIG}" +TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d) # Function to install tmux install_tmux() { @@ -73,7 +73,7 @@ setup_tmux_config() { mkdir -p "$config_dir" if [ -n "$TMUX_CONFIG" ]; then - printf "$TMUX_CONFIG" > "$config_file" + printf "%s" "$TMUX_CONFIG" > "$config_file" printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n" else cat > "$config_file" << EOF diff --git a/registry/coder/modules/filebrowser/README.md b/registry/coder/modules/filebrowser/README.md index 19120fed..80ab3e85 100644 --- a/registry/coder/modules/filebrowser/README.md +++ b/registry/coder/modules/filebrowser/README.md @@ -14,7 +14,7 @@ A file browser for your workspace. module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/filebrowser/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id } ``` @@ -29,7 +29,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/filebrowser/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -41,7 +41,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/filebrowser/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id database_path = ".config/filebrowser.db" } @@ -53,7 +53,7 @@ module "filebrowser" { module "filebrowser" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/filebrowser/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id agent_name = "main" subdomain = false diff --git a/registry/coder/modules/filebrowser/run.sh b/registry/coder/modules/filebrowser/run.sh index ed34e2a2..183c64a7 100644 --- a/registry/coder/modules/filebrowser/run.sh +++ b/registry/coder/modules/filebrowser/run.sh @@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH} fi -filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} +filebrowser config set --baseURL=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH} printf "👷 Starting filebrowser in background... \n\n" diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 72dc6f78..4df26ce7 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id } ``` @@ -37,7 +37,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id } ``` @@ -48,7 +48,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" @@ -61,7 +61,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id port = 8080 } @@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id use_cached = true } @@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed): module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.0.5" + version = "1.0.6" agent_id = coder_agent.main.id install = false } diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index be759a0a..2409f19d 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -97,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then fi # sed-based fallback if [ -z "$TARBALL_URL" ]; then - TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)" + TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -n1)" fi # Fallback: resolve version then construct tarball URL if [ -z "$TARBALL_URL" ]; then @@ -106,10 +106,10 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')" fi if [ -z "$RESOLVED_VERSION" ]; then - RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)" + RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)" fi if [ -z "$RESOLVED_VERSION" ]; then - RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)" + RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '"version":"[^"]*"' | head -n1 | cut -d '"' -f4)" fi if [ -n "$RESOLVED_VERSION" ]; then VERSION_TO_USE="$RESOLVED_VERSION" @@ -141,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then fi if [ -z "$BIN_PATH" ]; then # sed fallbacks (handle both string and object forms) - BIN_PATH=$(sed -n 's/.*\"bin\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' "$TMP_DIR/package/package.json" | head -n1) + BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1) if [ -z "$BIN_PATH" ]; then - BIN_PATH=$(sed -n '/\"bin\"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1) + BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"mux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) fi fi if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then diff --git a/registry/mossylion/.images/avatar.png b/registry/mossylion/.images/avatar.png new file mode 100644 index 00000000..a7f00f17 Binary files /dev/null and b/registry/mossylion/.images/avatar.png differ diff --git a/registry/mossylion/README.md b/registry/mossylion/README.md new file mode 100644 index 00000000..6f49a22c --- /dev/null +++ b/registry/mossylion/README.md @@ -0,0 +1,15 @@ +--- +display_name: "Mossy Lion" +bio: "Tinkerer, exploring European cloud providers" +avatar: "./.images/avatar.png" +github: "mossylion" +status: "community" +--- + +# Mossy Lion + +Exploring European cloud providers. Usually find me outdoors but if not, somewhere deep in Kubernetes and infra + +## Templates + +- **scaleway-instance**: Scaleway workspace instance with persistent home directory diff --git a/registry/mossylion/templates/scaleway-instance/README.md b/registry/mossylion/templates/scaleway-instance/README.md new file mode 100644 index 00000000..065c9311 --- /dev/null +++ b/registry/mossylion/templates/scaleway-instance/README.md @@ -0,0 +1,156 @@ +--- +display_name: "Scaleway Instance" +description: "A workspace spun up on a Scaleway Instance" +icon: "../../../../.icons/scaleway.svg" +verified: false +tags: ["scaleway", "vm", "linux"] +--- + +# Scaleway Instance Template + +This template provisions Coder workspaces on [Scaleway](https://www.scaleway.com/) cloud instances with full customization options for regions, instance types, operating systems, and storage configurations. + +## Features + +- **Multi-region support**: Choose from France (Paris), Netherlands (Amsterdam), or Poland (Warsaw) +- **Flexible instance sizing**: Wide range of instance types from development to high-performance computing +- **Multiple OS options**: Debian 12/13, Ubuntu 24.04, and Fedora 41 +- **Customizable storage**: Adjustable disk size with configurable IOPS +- **IPv4 and IPv6 networking**: Dual-stack IP configuration for enhanced connectivity + +## Prerequisites + +### Scaleway Account Setup + +1. Create a [Scaleway account](https://console.scaleway.com/) +2. Create a new project or use an existing one +3. Generate API credentials: + - Go to **IAM** > **API Keys** in the Scaleway Console + - Create a new API key + - Note down the **Access Key** and **Secret Key** + - Copy your **Project ID** from the project settings + - Give permissions for **BlockStorageFullAccess**, **ProjectReadOnly**, **InstancesFullAccess** as a starting point + +## Architecture + +This template creates the following resources for each workspace: + +### Persistent Resources + +- **Block Volume**: Mounted as user's home directory (preserves all data, configs, and projects) + +### Ephemeral Resources (destroyed when workspace stops) + +- **Scaleway Instance**: Virtual machine created fresh on each workspace start +- **IPv4 Address**: Routed IPv4 address assigned dynamically +- **IPv6 Address**: Routed IPv6 address assigned dynamically +- **Cloud-init Configuration**: Automated setup of the Coder agent and persistent storage mounting + +## Configuration Options + +### Region Selection + +Choose from three available regions: + +- **France - Paris (fr-par)**: Default, lowest latency for European users +- **Netherlands - Amsterdam (nl-ams)**: Alternative European location +- **Poland - Warsaw (pl-waw)**: Eastern European option + +### Instance Types + +The template supports a comprehensive range of Scaleway instance types: + +#### Development Instances + +- **STARDUST1-S**: 1 CPU, 1GB RAM - Basic development +- **DEV1-S/M/L/XL**: 2-4 CPUs, 2-12GB RAM - Standard development + +#### Production Instances + +- **ENT1 Series**: 2-96 CPUs, 8-384GB RAM - Enterprise workloads +- **GP1 Series**: 4-48 CPUs, 16-256GB RAM - General purpose +- **PRO2 Series**: 2-32 CPUs, 8-128GB RAM - Professional workloads + +#### Specialized Instances + +- **L4 Series**: GPU-enabled instances for AI/ML workloads +- **COPARM1 Series**: ARM64 architecture for specific use cases + +### Operating System Options + +- **Debian 13 (Trixie)**: Latest Debian release +- **Debian 12 (Bookworm)**: Stable Debian LTS +- **Ubuntu 24.04 (Noble)**: Latest Ubuntu LTS +- **Fedora 41**: Cutting-edge features and packages + +### Storage Configuration + +- **Home Directory Size**: 10-500GB adjustable via slider (your entire home directory) +- **IOPS**: 5,000 or 15,000 IOPS options for performance tuning + +## Template Components + +### Included Tools + +- **VS Code Server**: Browser-based IDE with full extension support +- **System Monitoring**: CPU, RAM, and disk usage metrics +- **Dotfiles Support**: Automatic dotfiles synchronization on workspace start +- **Custom Environment Variables**: Pre-configured welcome message + +### Cloud-init Setup + +The template uses cloud-init for: + +- Automatic Coder agent installation and configuration +- User account setup with proper permissions +- Persistent home directory mounting (automatic disk partitioning and filesystem creation) +- Development tools initialization + +## Usage + +### Creating a Workspace + +1. **Select Template**: Choose "Scaleway Instance" from your Coder templates +2. **Configure Region**: Pick your preferred Scaleway region +3. **Choose Instance**: Select instance type based on your performance needs +4. **Select OS**: Pick your preferred operating system +5. **Set Home Directory Size**: Adjust storage size (10-500GB) for your persistent home directory +6. **Create**: Launch your workspace + +### Managing Costs + +- **VM instances are destroyed** when workspace stops (zero compute costs when not in use) +- **IP addresses are released** when workspace stops (no static IP charges) +- **Home directory persists** on dedicated block volume (small storage cost only) +- **Fresh OS** on each workspace start with persistent user data +- Choose appropriate instance sizes for your workload requirements + +## Customization + +### Extending the Template + +You can customize this template by: + +1. **Adding Software**: Modify cloud-init scripts to install additional tools +2. **Custom Modules**: Include additional Coder modules from the registry +3. **Network Configuration**: Adjust security groups or network settings +4. **Startup Scripts**: Add custom initialization logic + +## Maintenance + +### Updating Instance Types + +To update the available instance types, regenerate the `scaleway-config.json` file: + +```bash +scw instance server-type list -o json | jq 'map({name, cpu, gpu, ram, arch})' > scaleway-config.json.json +``` + +This pulls the latest instance types from Scaleway and formats them for use in the template. + +## References + +- [Scaleway Documentation](https://www.scaleway.com/en/docs/) +- [Scaleway Instance Types](https://www.scaleway.com/en/pricing/#instances) +- [Coder Templates Documentation](https://coder.com/docs/templates) +- [Terraform Scaleway Provider](https://registry.terraform.io/providers/scaleway/scaleway/latest/docs) diff --git a/registry/mossylion/templates/scaleway-instance/cloud-init/cloud-config.yaml.tftpl b/registry/mossylion/templates/scaleway-instance/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 00000000..3aeb35ce --- /dev/null +++ b/registry/mossylion/templates/scaleway-instance/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,35 @@ +#cloud-config +cloud_final_modules: + - [scripts-user, always] +hostname: ${hostname} +users: + - name: ${linux_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + +# Setup persistent storage disk +disk_setup: + /dev/sdb: + table_type: gpt + layout: true + overwrite: false + +fs_setup: + - label: persistent-home + filesystem: ext4 + device: /dev/sdb1 + partition: auto + +mounts: + - ["/dev/sdb1", "/home/${linux_user}", "ext4", "defaults", "0", "2"] + +# Fix ownership after mounting +runcmd: + - chown -R ${linux_user}:${linux_user} /home/${linux_user} + - chmod 755 /home/${linux_user} + +# Automatically grow the partition +growpart: + mode: auto + devices: ['/'] + ignore_growroot_disabled: false diff --git a/registry/mossylion/templates/scaleway-instance/cloud-init/userdata.sh.tftpl b/registry/mossylion/templates/scaleway-instance/cloud-init/userdata.sh.tftpl new file mode 100644 index 00000000..72819ce9 --- /dev/null +++ b/registry/mossylion/templates/scaleway-instance/cloud-init/userdata.sh.tftpl @@ -0,0 +1,2 @@ +#!/bin/bash +sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}' diff --git a/registry/mossylion/templates/scaleway-instance/main.tf b/registry/mossylion/templates/scaleway-instance/main.tf new file mode 100644 index 00000000..a4ef968a --- /dev/null +++ b/registry/mossylion/templates/scaleway-instance/main.tf @@ -0,0 +1,337 @@ + +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 2" + } + scaleway = { + source = "scaleway/scaleway" + version = "~> 2" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2" + } + } + required_version = ">= 1.0" +} + +provider "scaleway" { + access_key = var.access_key + secret_key = var.secret_key + region = data.coder_parameter.region.value +} + +locals { + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + linux_user = local.linux_user + init_script = coder_agent.main.init_script + coder_agent_token = coder_agent.main.token + }) + } +} +data "coder_provisioner" "me" {} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + arch = local.selected_arch + os = data.coder_provisioner.me.os + auth = "token" + + startup_script = <<-EOT + set -e + + # Install additional tools or run commands at workspace startup + # Uncomment and customize as needed: + # sudo apt-get update + # sudo apt-get install -y build-essential + EOT + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + metadata { + display_name = "Disk Usage" + key = "1_disk_usage" + script = "coder stat disk --path /home/${local.linux_user}" + interval = 600 + timeout = 30 + } +} + +module "code-server" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.3.1" + agent_id = coder_agent.main.id + order = 1 + folder = "/home/${local.linux_user}" +} + +# Runs a script at workspace start/stop or on a cron schedule +# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script + +module "dotfiles" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/dotfiles/coder" + version = "1.2.1" + agent_id = coder_agent.main.id +} + +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = scaleway_instance_server.workspace[0].id + + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "instance type" + value = scaleway_instance_server.workspace[0].type + } + item { + key = "image" + value = data.coder_parameter.base_image.value + } +} + +resource "coder_metadata" "volume_info" { + resource_id = scaleway_block_volume.persistent_storage.id + + item { + key = "size" + value = "${scaleway_block_volume.persistent_storage.size_in_gb} GiB" + } + item { + key = "iops" + value = scaleway_block_volume.persistent_storage.iops + } +} + +data "coder_parameter" "region" { + name = "Scaleway Region" + description = "Region to deploy server into" + type = "string" + default = "fr-par" + option { + name = "France - Paris (fr-par)" + value = "fr-par" + icon = "/emojis/1f1eb-1f1f7.png" + } + option { + name = "Netherlands - Amsterdam (nl-ams)" + value = "nl-ams" + icon = "/emojis/1f1f3-1f1f1.png" + } + option { + name = "Poland - Warsaw (pl-waw)" + value = "pl-waw" + icon = "/emojis/1f1f5-1f1f1.png" + } +} + +data "coder_parameter" "base_image" { + name = "Image" + description = "Which base image would you like to use?" + type = "string" + form_type = "radio" + default = "debian_trixie" + + option { + name = "Debian 13 (Trixie)" + value = "debian_trixie" + icon = "/icon/debian.svg" + } + + option { + name = "Debian 12 (Bookworm)" + value = "debian_bookworm" + icon = "/icon/debian.svg" + } + + option { + name = "Ubuntu 24.04 (Noble)" + value = "ubuntu_noble" + icon = "/icon/ubuntu.svg" + } + + option { + name = "Fedora 41" + value = "fedora_41" + icon = "/icon/fedora.svg" + } +} + +data "coder_parameter" "root_volume_size" { + name = "Root Volume Size" + description = "Size of the OS/boot disk in GB" + type = "number" + form_type = "slider" + default = "20" + order = 7 + validation { + min = 10 + max = 1000 + monotonic = "increasing" + } +} + +data "coder_parameter" "disk_size" { + name = "Persistent Storage Size" + description = "Size of the additional persistent storage volume in GB" + type = "number" + form_type = "slider" + default = "10" + order = 8 + validation { + min = 10 + max = 500 + monotonic = "increasing" + } +} + +locals { + scaleway_config_raw = jsondecode(file("${path.module}/scaleway-config.json")) + + scaleway_instance_options = { + for instance in local.scaleway_config_raw : + instance.name => { + name = "${instance.name} (${instance.cpu} CPU, ${instance.gpu} GPU, ${floor(instance.ram / 1073741824)} GB RAM)" + value = instance.name + } + } + + instance_arch_map = { + for instance in local.scaleway_config_raw : + instance.name => instance.arch + } + + # Convert Scaleway arch format to Coder arch format + selected_arch = local.instance_arch_map[data.coder_parameter.instance_size.value] == "x86_64" ? "amd64" : local.instance_arch_map[data.coder_parameter.instance_size.value] +} + +data "coder_parameter" "instance_size" { + name = "instance_size" + display_name = "Instance Size" + description = "Which Instance Size should be used?" + default = "DEV1-M" + type = "string" + icon = "/icon/memory.svg" + mutable = false + form_type = "dropdown" + + dynamic "option" { + for_each = local.scaleway_instance_options + content { + name = option.value.name + value = option.value.value + } + } +} + +data "coder_parameter" "volume_iops" { + name = "Volume IOPS" + description = "IOPS to provision for disk" + type = "number" + default = 5000 + option { + name = "5000" + value = 5000 + } + option { + name = "15000" + value = 15000 + } +} + +resource "scaleway_instance_server" "workspace" { + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + type = data.coder_parameter.instance_size.value + image = data.coder_parameter.base_image.value + ip_ids = [scaleway_instance_ip.server_ip[0].id, scaleway_instance_ip.v4_server_ip[0].id] + project_id = var.project_id + user_data = { + cloud-init = data.cloudinit_config.user_data.rendered + } + additional_volume_ids = [scaleway_block_volume.persistent_storage.id] + + root_volume { + size_in_gb = data.coder_parameter.root_volume_size.value + } +} + +resource "scaleway_block_volume" "persistent_storage" { + iops = data.coder_parameter.volume_iops.value + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home" + size_in_gb = data.coder_parameter.disk_size.value + project_id = var.project_id +} + + +resource "scaleway_instance_ip" "server_ip" { + count = data.coder_workspace.me.start_count + type = "routed_ipv6" + project_id = var.project_id +} + +resource "scaleway_instance_ip" "v4_server_ip" { + count = data.coder_workspace.me.start_count + type = "routed_ipv4" + project_id = var.project_id +} + +variable "project_id" { + type = string + description = "ID of the project to deploy into" +} + +variable "access_key" { + type = string + description = "Access key to use to deploy" +} + +variable "secret_key" { + type = string + description = "Secret key to use to deploy" +} diff --git a/registry/mossylion/templates/scaleway-instance/scaleway-config.json b/registry/mossylion/templates/scaleway-instance/scaleway-config.json new file mode 100644 index 00000000..e48fb05c --- /dev/null +++ b/registry/mossylion/templates/scaleway-instance/scaleway-config.json @@ -0,0 +1,450 @@ +[ + { + "name": "COPARM1-2C-8G", + "cpu": 2, + "gpu": 0, + "ram": 8589934592, + "arch": "arm64" + }, + { + "name": "COPARM1-4C-16G", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "arm64" + }, + { + "name": "COPARM1-8C-32G", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "arm64" + }, + { + "name": "COPARM1-16C-64G", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "arm64" + }, + { + "name": "COPARM1-32C-128G", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "arm64" + }, + { + "name": "DEV1-S", + "cpu": 2, + "gpu": 0, + "ram": 2147483648, + "arch": "x86_64" + }, + { + "name": "DEV1-M", + "cpu": 3, + "gpu": 0, + "ram": 4294967296, + "arch": "x86_64" + }, + { + "name": "DEV1-L", + "cpu": 4, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "DEV1-XL", + "cpu": 4, + "gpu": 0, + "ram": 12884901888, + "arch": "x86_64" + }, + { + "name": "ENT1-XXS", + "cpu": 2, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "ENT1-XS", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "ENT1-S", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "ENT1-M", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "ENT1-L", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "ENT1-XL", + "cpu": 64, + "gpu": 0, + "ram": 274877906944, + "arch": "x86_64" + }, + { + "name": "ENT1-2XL", + "cpu": 96, + "gpu": 0, + "ram": 412316860416, + "arch": "x86_64" + }, + { + "name": "GP1-XS", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "GP1-S", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "GP1-M", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "GP1-L", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "GP1-XL", + "cpu": 48, + "gpu": 0, + "ram": 274877906944, + "arch": "x86_64" + }, + { + "name": "L4-1-24G", + "cpu": 8, + "gpu": 1, + "ram": 51539607552, + "arch": "x86_64" + }, + { + "name": "L4-2-24G", + "cpu": 16, + "gpu": 2, + "ram": 103079215104, + "arch": "x86_64" + }, + { + "name": "L4-4-24G", + "cpu": 32, + "gpu": 4, + "ram": 206158430208, + "arch": "x86_64" + }, + { + "name": "L4-8-24G", + "cpu": 64, + "gpu": 8, + "ram": 412316860416, + "arch": "x86_64" + }, + { + "name": "PLAY2-PICO", + "cpu": 1, + "gpu": 0, + "ram": 2147483648, + "arch": "x86_64" + }, + { + "name": "PLAY2-NANO", + "cpu": 2, + "gpu": 0, + "ram": 4294967296, + "arch": "x86_64" + }, + { + "name": "PLAY2-MICRO", + "cpu": 4, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-HC-2C-4G", + "cpu": 2, + "gpu": 0, + "ram": 4294967296, + "arch": "x86_64" + }, + { + "name": "POP2-2C-8G", + "cpu": 2, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-HM-2C-16G", + "cpu": 2, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "POP2-HC-4C-8G", + "cpu": 4, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-4C-16G", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "POP2-2C-8G-WIN", + "cpu": 2, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-HM-4C-32G", + "cpu": 4, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "POP2-HC-8C-16G", + "cpu": 8, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "POP2-HN-3", + "cpu": 2, + "gpu": 0, + "ram": 4294967296, + "arch": "x86_64" + }, + { + "name": "POP2-8C-32G", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "POP2-4C-16G-WIN", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "POP2-HM-8C-64G", + "cpu": 8, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "POP2-HC-16C-32G", + "cpu": 16, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "POP2-HN-5", + "cpu": 4, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-16C-64G", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "POP2-8C-32G-WIN", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "POP2-HN-10", + "cpu": 4, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "POP2-HM-16C-128G", + "cpu": 16, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "POP2-HC-32C-64G", + "cpu": 32, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "POP2-32C-128G", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "POP2-HC-48C-96G", + "cpu": 48, + "gpu": 0, + "ram": 103079215104, + "arch": "x86_64" + }, + { + "name": "POP2-16C-64G-WIN", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "POP2-HM-32C-256G", + "cpu": 32, + "gpu": 0, + "ram": 274877906944, + "arch": "x86_64" + }, + { + "name": "POP2-HC-64C-128G", + "cpu": 64, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "POP2-48C-192G", + "cpu": 48, + "gpu": 0, + "ram": 206158430208, + "arch": "x86_64" + }, + { + "name": "POP2-64C-256G", + "cpu": 64, + "gpu": 0, + "ram": 274877906944, + "arch": "x86_64" + }, + { + "name": "POP2-HM-48C-384G", + "cpu": 48, + "gpu": 0, + "ram": 412316860416, + "arch": "x86_64" + }, + { + "name": "POP2-32C-128G-WIN", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "POP2-HM-64C-512G", + "cpu": 64, + "gpu": 0, + "ram": 549755813888, + "arch": "x86_64" + }, + { + "name": "PRO2-XXS", + "cpu": 2, + "gpu": 0, + "ram": 8589934592, + "arch": "x86_64" + }, + { + "name": "PRO2-XS", + "cpu": 4, + "gpu": 0, + "ram": 17179869184, + "arch": "x86_64" + }, + { + "name": "PRO2-S", + "cpu": 8, + "gpu": 0, + "ram": 34359738368, + "arch": "x86_64" + }, + { + "name": "PRO2-M", + "cpu": 16, + "gpu": 0, + "ram": 68719476736, + "arch": "x86_64" + }, + { + "name": "PRO2-L", + "cpu": 32, + "gpu": 0, + "ram": 137438953472, + "arch": "x86_64" + }, + { + "name": "RENDER-S", + "cpu": 10, + "gpu": 1, + "ram": 45097156608, + "arch": "x86_64" + }, + { + "name": "STARDUST1-S", + "cpu": 1, + "gpu": 0, + "ram": 1073741824, + "arch": "x86_64" + } +] diff --git a/registry/nboyers/.gitignore b/registry/nboyers/.gitignore new file mode 100644 index 00000000..9a58485f --- /dev/null +++ b/registry/nboyers/.gitignore @@ -0,0 +1,41 @@ +# Local and OS files +.DS_Store +Thumbs.db +*.log +*.tmp +*.swp +*.bak + +# Terraform +.terraform/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup +crash.log + +# Node / Bun / Python / other tool artifacts +node_modules/ +bun.lockb +package-lock.json +__pycache__/ +*.pyc + +# Cloud credentials and keys +*.pem +*.key +*.p12 +*.json +*.env +.envrc +aws-credentials +gcp.json +azure-creds.json + +# Archives +*.zip +*.tar.gz +*.tgz + +# Workspace artifacts +workspace/ +output/ diff --git a/registry/nboyers/.images/avatar.png b/registry/nboyers/.images/avatar.png new file mode 100644 index 00000000..546fbd89 Binary files /dev/null and b/registry/nboyers/.images/avatar.png differ diff --git a/registry/nboyers/README.md b/registry/nboyers/README.md new file mode 100644 index 00000000..57dc2bca --- /dev/null +++ b/registry/nboyers/README.md @@ -0,0 +1,14 @@ +--- +display_name: "Noah Boyers" +bio: "Cloud & DevOps engineer with an MBA, building scalable multi-cloud infrastructure." +avatar: "./.images/avatar.png" +github: "noahboyers" +linkedin: "https://www.linkedin.com/in/nboyers" +website: "https://nobosoftware.com" +support_email: "hello@nobosoftware.com" +status: "community" +--- + +# Noah Boyers + +Cloud and DevOps engineer focused on scalable, secure, and automated infrastructure across AWS, Azure, and GCP. diff --git a/registry/nboyers/templates/cloud-dev/README.md b/registry/nboyers/templates/cloud-dev/README.md new file mode 100644 index 00000000..257faf16 --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/README.md @@ -0,0 +1,80 @@ +--- +display_name: "Cloud DevOps Workspace" +description: "A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP." +icon: "https://raw.githubusercontent.com/coder/coder-icons/main/icons/cloud-devops.svg" +tags: + - devops + - kubernetes + - aws + - eks + - multi-cloud + - terraform + - cdk + - pulumi +--- + +# Cloud DevOps Workspace + +A secure, company-standard DevOps environment for platform and cloud engineers. + +This template deploys workspaces **into an existing Amazon EKS cluster** and provides developers with tools and credentials to work with **AWS, Azure, and GCP** from inside their workspace. + +Supports multiple Infrastructure-as-Code frameworks — **Terraform**, **AWS CDK**, and **Pulumi** — for flexible, multi-cloud development. + +## Features + +- **Multi-Cloud Ready** — authenticate to AWS, Azure, or GCP from a single workspace +- **Runs on EKS** — leverages existing Kubernetes infrastructure for scaling and security +- **IaC Tools Included** — Terraform, Terragrunt, CDK, Pulumi, tfsec, and more +- **Secure Isolation** — each workspace runs in its own Kubernetes namespace +- **Configurable Auth** — supports IRSA (AWS), Federated Identity (Azure), and WIF (GCP) + +## Variables + +| Variable | Description | Type | Default | +| ------------------------------------------------------------- | --------------------------------------------------------------- | ------ | ----------- | +| `host_cluster_name` | EKS cluster name where workspaces are deployed | string | — | +| `iac_tool` | Infrastructure-as-Code framework (`terraform`, `cdk`, `pulumi`) | string | `terraform` | +| `enable_aws` | Enable AWS authentication and tools | bool | `true` | +| `enable_azure` | Enable Azure authentication and tools | bool | `false` | +| `enable_gcp` | Enable GCP authentication and tools | bool | `false` | +| `aws_access_key_id` / `aws_secret_access_key` | AWS credentials (optional) | string | `""` | +| `azure_client_id` / `azure_client_secret` / `azure_tenant_id` | Azure credentials (optional) | string | `""` | +| `gcp_service_account` | GCP Service Account JSON (optional) | string | `""` | + +## Runtime Architecture + +| Layer | Platform | Purpose | +| ----------------------- | ------------------ | ------------------------------------------------------------ | +| **Infrastructure** | Amazon EKS | Where Coder deploys and runs the workspaces | +| **Workspace Container** | Ubuntu-based image | Developer environment (Terraform, CDK, Pulumi, CLIs) | +| **Cloud Access** | AWS / Azure / GCP | Target environments for deploying infrastructure or services | + +## Required Permissions and Setup Steps + +This template **runs on EKS** but allows developers inside the workspace to authenticate with **AWS, Azure, or GCP** using their own credentials or service identities. + +### Coder & Infrastructure (Admin Setup) + +Your Coder deployment must have: + +- Network access to an **existing EKS cluster** +- The Coder Helm chart installed and healthy +- Terraform configured with access to the EKS API + +#### Minimum AWS IAM Permissions + +For the identity running the template (Coder service account, Terraform runner, or user): + +```json +{ + "Effect": "Allow", + "Action": [ + "eks:DescribeCluster", + "eks:ListClusters", + "sts:GetCallerIdentity", + "sts:AssumeRole" + ], + "Resource": "*" +} +``` diff --git a/registry/nboyers/templates/cloud-dev/main.tf b/registry/nboyers/templates/cloud-dev/main.tf new file mode 100644 index 00000000..67b64cf0 --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/main.tf @@ -0,0 +1,120 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 0.23" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.23" + } + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# --- Coder workspace context --- +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +# --- EKS connection --- +data "aws_eks_cluster" "eks" { + name = trimspace(var.host_cluster_name) +} + + +data "aws_eks_cluster_auth" "eks" { + name = trimspace(var.host_cluster_name) +} + +provider "kubernetes" { + host = data.aws_eks_cluster.eks.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.eks.token +} + +# --- Namespace per workspace --- +resource "kubernetes_namespace" "workspace" { + metadata { + name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + labels = { + "coder.workspace" = data.coder_workspace.me.name + "coder.owner" = data.coder_workspace_owner.me.name + } + } +} + +# --- ServiceAccount (IRSA optional) --- +resource "kubernetes_service_account" "workspace" { + metadata { + name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + namespace = kubernetes_namespace.workspace.metadata[0].name + + annotations = var.enable_aws && var.aws_role_arn != "" ? { + "eks.amazonaws.com/role-arn" = var.aws_role_arn + } : {} + } +} + +# --- Coder Agent definition --- +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + startup_script = file("${path.module}/scripts/setup-workspace.sh") + + env = { + # IaC tool & cloud toggles + IAC_TOOL = var.iac_tool + ENABLE_AWS = tostring(var.enable_aws) + ENABLE_AZURE = tostring(var.enable_azure) + ENABLE_GCP = tostring(var.enable_gcp) + + # Developer credentials + AWS_ACCESS_KEY_ID = var.aws_access_key_id + AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key + AZURE_CLIENT_ID = var.azure_client_id + AZURE_TENANT_ID = var.azure_tenant_id + AZURE_CLIENT_SECRET = var.azure_client_secret + GCP_SERVICE_ACCOUNT = var.gcp_service_account + } +} + +# --- Kubernetes Pod (runs workspace container) --- +resource "kubernetes_pod" "workspace" { + count = data.coder_workspace.me.start_count + + metadata { + name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + namespace = kubernetes_namespace.workspace.metadata[0].name + labels = { + "app" = "coder-workspace" + "coder.owner" = data.coder_workspace_owner.me.name + "coder.agent" = "true" + } + } + + spec { + service_account_name = kubernetes_service_account.workspace.metadata[0].name + + container { + name = "workspace" + image = "codercom/enterprise-base:ubuntu" + command = ["/bin/bash", "-c", coder_agent.main.init_script] + + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + + resources { + requests = { cpu = "500m", memory = "1Gi" } + limits = { cpu = "2", memory = "4Gi" } + } + } + } + + depends_on = [coder_agent.main] +} diff --git a/registry/nboyers/templates/cloud-dev/scripts/cloud-auth.sh b/registry/nboyers/templates/cloud-dev/scripts/cloud-auth.sh new file mode 100644 index 00000000..18550bb0 --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/scripts/cloud-auth.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +# cloud-auth.sh — Multi-cloud auth helpers (source this file, don't execute) +# Supports: +# - AWS: access keys or IRSA (via pod SA) +# - Azure: federated token or client secret +# - GCP: service account JSON or Workload Identity Federation (KSA -> SA) + +set -euo pipefail + +# -------- util -------- +_has() { command -v "$1" > /dev/null 2>&1; } +_docker_ok() { _has docker && [[ -S /var/run/docker.sock ]]; } + +cloud-auth-help() { + cat << 'EOHELP' +Multi-Cloud Authentication Helper — source this file: + + source ~/workspace/cloud-auth.sh + +Environment variables (read if set): + + # Common toggles (optional) + ENABLE_AWS=true|false + ENABLE_AZURE=true|false + ENABLE_GCP=true|false + + # AWS + AWS_REGION=us-west-2 + AWS_ACCESS_KEY_ID=... + AWS_SECRET_ACCESS_KEY=... + AWS_SESSION_TOKEN=... # optional (STS); if unset, IRSA/IMDS is used + + # Azure + AZURE_CLIENT_ID=... + AZURE_TENANT_ID=... + AZURE_CLIENT_SECRET=... # OR: + AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token + + # GCP + GCP_PROJECT_ID=... + # Option A (Service Account JSON): + GCP_SERVICE_ACCOUNT='{ ... }' + # Option B (Workload Identity Federation): + GCP_WORKLOAD_IDENTITY_PROVIDER=projects/..../locations/global/workloadIdentityPools/.../providers/... + # (uses KSA token at /var/run/secrets/kubernetes.io/serviceaccount/token) + +Functions: + + # AWS + aws-login # ensures creds (keys or IRSA), sets region config if provided + aws-check # prints caller identity + aws-ecr-login # docker login to ECR (if docker socket present) + + # Azure + azure-login # SP login via federated token OR client secret + azure-check # prints account info + azure-acr-login # docker login to ACR (requires AZURE_ACR_NAME) + + # GCP + gcp-login # SA JSON or WIF + gcp-check # prints active gcloud account & project + gcp-gar-login # docker auth to GAR (requires GCP_REGION & PROJECT) + + # Convenience + multicloud-login # calls the per-cloud logins if toggles are true + multicloud-check # calls the per-cloud checks +EOHELP +} + +# -------- AWS -------- +aws-login() { + [[ "${ENABLE_AWS:-true}" == "true" ]] || { + echo "AWS disabled" + return 0 + } + if ! _has aws; then + echo "aws CLI not found" + return 1 + fi + + # If access keys are present, write standard files; otherwise rely on IRSA/IMDS + if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then + mkdir -p "${HOME}/.aws" + { + echo "[default]" + echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}" + echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}" + [[ -n "${AWS_SESSION_TOKEN:-}" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}" + } > "${HOME}/.aws/credentials" + if [[ -n "${AWS_REGION:-}" ]]; then + { + echo "[default]" + echo "region=${AWS_REGION}" + } > "${HOME}/.aws/config" + fi + fi + + # Validate + if ! aws sts get-caller-identity > /dev/null 2>&1; then + echo "❌ AWS auth failed (neither valid keys nor IRSA available)" + return 1 + fi + echo "✅ AWS auth OK" +} + +aws-check() { + _has aws || { + echo "aws CLI not found" + return 1 + } + aws sts get-caller-identity +} + +aws-ecr-login() { + _has aws || { + echo "aws CLI not found" + return 1 + } + _docker_ok || { + echo "â„šī¸ docker socket not available; skipping ECR login" + return 0 + } + : "${AWS_REGION:=us-east-1}" + aws-login > /dev/null || return 1 + AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)" + aws ecr get-login-password --region "${AWS_REGION}" \ + | docker login --username AWS --password-stdin \ + "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + echo "✅ ECR login OK → ${ECR_REGISTRY}" +} + +# -------- Azure -------- +azure-login() { + [[ "${ENABLE_AZURE:-false}" == "true" ]] || { + echo "Azure disabled" + return 0 + } + _has az || { + echo "az CLI not found" + return 1 + } + [[ -n "${AZURE_CLIENT_ID:-}" && -n "${AZURE_TENANT_ID:-}" ]] || { + echo "❌ Set AZURE_CLIENT_ID and AZURE_TENANT_ID" + return 1 + } + + if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then + az login --service-principal \ + --username "${AZURE_CLIENT_ID}" \ + --tenant "${AZURE_TENANT_ID}" \ + --federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \ + --allow-no-subscriptions + elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then + az login --service-principal \ + -u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" \ + --tenant "${AZURE_TENANT_ID}" + else + echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET" + return 1 + fi + + echo "✅ Azure auth OK" +} + +azure-check() { + _has az || { + echo "az CLI not found" + return 1 + } + az account show +} + +azure-acr-login() { + _has az || { + echo "az CLI not found" + return 1 + } + _docker_ok || { + echo "â„šī¸ docker socket not available; skipping ACR login" + return 0 + } + [[ -n "${AZURE_ACR_NAME:-}" ]] || { + echo "❌ Set AZURE_ACR_NAME" + return 1 + } + az account show > /dev/null 2>&1 || azure-login + az acr login --name "${AZURE_ACR_NAME}" + export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io" + echo "✅ ACR login OK → ${ACR_REGISTRY}" +} + +# -------- GCP -------- +gcp-login() { + [[ "${ENABLE_GCP:-false}" == "true" ]] || { + echo "GCP disabled" + return 0 + } + _has gcloud || { + echo "gcloud not found" + return 1 + } + + if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then + # Service Account JSON path + echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || { + echo "❌ Failed to write GCP credentials" + return 1 + } + export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json || { + echo "❌ Failed to set GCP credentials path" + return 1 + } + gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || { + echo "❌ GCP service account auth failed" + return 1 + } + else + # Workload Identity Federation using KSA token + WIP provider + [[ -n "${GCP_WORKLOAD_IDENTITY_PROVIDER:-}" && -n "${GCP_PROJECT_ID:-}" ]] || { + echo "❌ Provide GCP_SERVICE_ACCOUNT JSON or set GCP_WORKLOAD_IDENTITY_PROVIDER & GCP_PROJECT_ID" + return 1 + } + [[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]] || { + echo "❌ KSA token not found" + return 1 + } + + TMP="/tmp/gcp-wif-$$.json" + cat > "${TMP}" << 'EOF' +{ + "type": "external_account", + "audience": "//iam.googleapis.com/${GCP_WORKLOAD_IDENTITY_PROVIDER}", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "credential_source": { + "file": "/var/run/secrets/kubernetes.io/serviceaccount/token", + "format": { "type": "text" } + } +} +EOF + [[ $? -eq 0 ]] || { + echo "❌ Failed to write GCP WIF config" + return 1 + } + export GOOGLE_APPLICATION_CREDENTIALS="${TMP}" || { + echo "❌ Failed to set GCP credentials path" + return 1 + } + gcloud auth login --cred-file="${GOOGLE_APPLICATION_CREDENTIALS}" --quiet || { + echo "❌ GCP WIF auth failed" + return 1 + } + fi + + if [[ -n "${GCP_PROJECT_ID:-}" ]]; then + gcloud config set project "${GCP_PROJECT_ID}" --quiet + fi + echo "✅ GCP auth OK" +} + +gcp-check() { + _has gcloud || { + echo "gcloud not found" + return 1 + } + gcloud auth list + gcloud config get-value project || true +} + +gcp-gar-login() { + _docker_ok || { + echo "â„šī¸ docker socket not available; skipping GAR login" + return 0 + } + : "${GCP_REGION:=us-central1}" + [[ -n "${GCP_PROJECT_ID:-}" ]] || { + echo "❌ Set GCP_PROJECT_ID" + return 1 + } + gcloud auth list --filter=status:ACTIVE --format="value(account)" > /dev/null || gcp-login + gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet + export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}" + echo "✅ GAR configured → ${GAR_REGISTRY}" +} + +# -------- Convenience -------- +multicloud-login() { + if [[ "${ENABLE_AWS:-true}" == "true" ]]; then + aws-login + fi + if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then + azure-login + fi + if [[ "${ENABLE_GCP:-false}" == "true" ]]; then + gcp-login + fi + echo "✨ Multi-cloud login complete" +} + +multicloud-check() { + if [[ "${ENABLE_AWS:-true}" == "true" ]]; then + echo "AWS:" + aws-check + echo + fi + if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then + echo "Azure:" + azure-check + echo + fi + if [[ "${ENABLE_GCP:-false}" == "true" ]]; then + echo "GCP:" + gcp-check + echo + fi +} + +echo "✨ cloud-auth loaded. Run 'cloud-auth-help' for usage." diff --git a/registry/nboyers/templates/cloud-dev/scripts/setup-workspace.sh b/registry/nboyers/templates/cloud-dev/scripts/setup-workspace.sh new file mode 100644 index 00000000..937cb9cc --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/scripts/setup-workspace.sh @@ -0,0 +1,501 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ========================= +# Helpers & safe defaults +# ========================= +log() { printf '%s %s\n' "👉" "$*"; } +ok() { printf '%s %s\n' "✅" "$*"; } +skip() { printf '%s %s\n' "â­ī¸" "$*"; } +warn() { printf '%s %s\n' "âš ī¸" "$*"; } + +# Detect CPU arch (amd64/arm64) +arch() { + case "$(uname -m)" in + x86_64 | amd64) echo amd64 ;; + aarch64 | arm64) echo arm64 ;; + *) echo amd64 ;; + esac +} + +# Map to Docker static tarball arch names +docker_tar_arch() { + case "$(arch)" in + amd64) echo x86_64 ;; + arm64) echo aarch64 ;; + *) echo x86_64 ;; + esac +} + +SAFE_TMP="$(mktemp -d)" +trap 'rm -rf "$SAFE_TMP"' EXIT + +safe_dl() { # url dest + curl -fL --retry 5 --retry-delay 2 --connect-timeout 10 -o "$2" "$1" || { + echo "Failed to download $1" + return 1 + } +} + +docker_ok() { + command -v docker > /dev/null 2>&1 && [[ -S /var/run/docker.sock ]] +} + +# Ensure user bin dir +mkdir -p "$HOME/.local/bin" "$HOME/workspace/app" +export PATH="$HOME/.local/bin:$PATH" + +# Inputs (with sane defaults) +IAC_TOOL="${IAC_TOOL:-terraform}" +TERRAFORM_VERSION="${TERRAFORM_VERSION:-1.6.0}" + +ENABLE_AWS="${ENABLE_AWS:-true}" +ENABLE_AZURE="${ENABLE_AZURE:-false}" +ENABLE_GCP="${ENABLE_GCP:-false}" + +AWS_REGION="${AWS_REGION:-}" +AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" +AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" +AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" + +AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-}" +AZURE_TENANT_ID="${AZURE_TENANT_ID:-}" +AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-}" +AZURE_FEDERATED_TOKEN_FILE="${AZURE_FEDERATED_TOKEN_FILE:-}" + +GCP_PROJECT_ID="${GCP_PROJECT_ID:-}" +GCP_SERVICE_ACCOUNT="${GCP_SERVICE_ACCOUNT:-}" # full JSON if not using WIF + +REPO_URL="${REPO_URL:-${repo_url:-}}" +DEFAULT_BRANCH="${DEFAULT_BRANCH:-${default_branch:-main}}" +WORKDIR="${WORKDIR:-$HOME/workspace/app}" +GITHUB_TOKEN="${GITHUB_TOKEN:-${GIT_TOKEN:-}}" + +GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-}" +GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-}" + +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ Multi-Cloud DevOps Workspace Setup (no sudo) ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo + +# ========================================================== +# Write multi-cloud helper functions to ~/workspace/cloud-auth.sh +# ========================================================== +cat > "${HOME}/workspace/cloud-auth.sh" << 'EOAUTHSCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +aws-ecr-login() { + : "${AWS_REGION:=us-east-1}" + if ! command -v aws >/dev/null 2>&1; then echo "aws CLI not found"; return 1; fi + if ! aws sts get-caller-identity &>/dev/null; then + echo "❌ AWS creds not available (IRSA or keys)"; return 1; fi + AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)" + if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then + aws ecr get-login-password --region "${AWS_REGION}" | \ + docker login --username AWS --password-stdin \ + "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + echo "✅ ECR login OK → ${ECR_REGISTRY}" + else + echo "â„šī¸ docker socket not available; skipping docker login" + fi +} + +aws-check() { aws sts get-caller-identity && echo "✓ AWS creds valid"; } + +azure-login() { + if ! command -v az >/dev/null 2>&1; then echo "az CLI not found"; return 1; fi + if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then + az login --service-principal --username "${AZURE_CLIENT_ID}" \ + --tenant "${AZURE_TENANT_ID}" \ + --federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \ + --allow-no-subscriptions + elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then + az login --service-principal -u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" --tenant "${AZURE_TENANT_ID}" + else + echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET"; return 1 + fi + echo "✅ Azure auth OK"; az account show +} + +azure-acr-login() { + [[ -n "${AZURE_ACR_NAME:-}" ]] || { echo "Set AZURE_ACR_NAME"; return 1; } + az account show &>/dev/null || azure-login + if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then + az acr login --name "${AZURE_ACR_NAME}" + export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io" + echo "✅ ACR login OK → ${ACR_REGISTRY}" + else + echo "â„šī¸ docker socket not available; skipping docker login" + fi +} + +azure-check() { az account show && echo "✓ Azure creds valid" || { echo "❌ Not logged in"; return 1; }; } + +gcp-login() { + if ! command -v gcloud >/dev/null 2>&1; then echo "gcloud not found"; return 1; fi + if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then + # SA JSON auth + echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || { echo "❌ Failed to write GCP credentials"; return 1; } + export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json + gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || { echo "❌ GCP auth failed"; return 1; } + else + echo "❌ Provide GCP_SERVICE_ACCOUNT JSON (WIF path not configured here)"; return 1 + fi + [[ -n "${GCP_PROJECT_ID:-}" ]] && gcloud config set project "${GCP_PROJECT_ID}" --quiet || true + echo "✅ GCP auth OK"; gcloud auth list +} + +gcp-gar-login() { + : "${GCP_REGION:=us-central1}" + [[ -n "${GCP_PROJECT_ID:-}" ]] || { echo "Set GCP_PROJECT_ID"; return 1; } + gcloud auth list --filter=status:ACTIVE --format="value(account)" &>/dev/null || gcp-login + if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then + gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet + export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}" + echo "✅ GAR configured → ${GAR_REGISTRY}" + else + echo "â„šī¸ docker socket not available; skipping docker login" + fi +} + +gcp-check() { gcloud auth list --filter=status:ACTIVE --format="value(account)" >/dev/null && echo "✓ GCP creds valid" || { echo "❌ Not logged in"; return 1; }; } + +multicloud-login() { + [[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && aws-ecr-login || true + [[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && azure-login || true + [[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && gcp-login || true + echo "✨ Multi-cloud login complete" +} + +multicloud-check() { + [[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && { echo "AWS:"; aws-check; echo; } || true + [[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && { echo "Azure:"; azure-check; echo; } || true + [[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && { echo "GCP:"; gcp-check; echo; } || true +} + +cloud-auth-help() { + cat <<'EOHELP' +Multi-Cloud Authentication Helper + +Functions: + AWS: aws-ecr-login, aws-check + Azure: azure-login, azure-acr-login, azure-check + GCP: gcp-login, gcp-gar-login, gcp-check + Multi: multicloud-login, multicloud-check, cloud-auth-help +EOHELP + return 0 +} + +echo "✨ Multi-cloud auth helpers loaded. Run 'cloud-auth-help' for help." +EOAUTHSCRIPT +chmod +x "${HOME}/workspace/cloud-auth.sh" +ok "Created ${HOME}/workspace/cloud-auth.sh" +echo + +# ========================= +# IaC tooling +# ========================= +log "Installing IaC tooling (${IAC_TOOL})" +case "$IAC_TOOL" in + terraform) + if ! command -v terraform > /dev/null 2>&1; then + safe_dl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_$(arch).zip" "$SAFE_TMP/tf.zip" + unzip -q "$SAFE_TMP/tf.zip" -d "$HOME/.local/bin" + ok "Terraform ${TERRAFORM_VERSION} installed" + else + ok "Terraform already installed ($(terraform version | head -1))" + fi + ;; + cdk) + if ! command -v npm > /dev/null 2>&1; then + log "npm not found; installing Node via nvm" + export NVM_DIR="$HOME/.nvm" + curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" + nvm install --lts + nvm use --lts + # persist for future shells + grep -q 'NVM_DIR' "$HOME/.bashrc" 2> /dev/null || { + echo 'export NVM_DIR="$HOME/.nvm"' >> "$HOME/.bashrc" + echo '. "$NVM_DIR/nvm.sh"' >> "$HOME/.bashrc" + } + fi + npm install -g aws-cdk > /dev/null + ok "AWS CDK installed ($(cdk --version))" + ;; + pulumi) + if ! command -v pulumi > /dev/null 2>&1; then + curl -fsSL https://get.pulumi.com | sh + export PATH="$PATH:$HOME/.pulumi/bin" + ok "Pulumi installed ($(pulumi version))" + else + ok "Pulumi already installed ($(pulumi version))" + fi + ;; + *) + warn "Unknown IAC_TOOL=${IAC_TOOL}; skipping IaC install" + ;; +esac + +# Extras: Terragrunt, tflint, tfsec, terraform-docs, pre-commit +if ! command -v terragrunt > /dev/null 2>&1; then + TG_VER="0.54.0" + safe_dl "https://github.com/gruntwork-io/terragrunt/releases/download/v${TG_VER}/terragrunt_linux_$(arch)" "$HOME/.local/bin/terragrunt" + chmod +x "$HOME/.local/bin/terragrunt" + ok "Terragrunt v${TG_VER} installed" +fi + +if ! command -v tflint > /dev/null 2>&1; then + # official installer handles arch + curl -fsSL https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash + mv -f /tmp/tflint "$HOME/.local/bin/" 2> /dev/null || true + ok "tflint installed" +fi + +if ! command -v tfsec > /dev/null 2>&1; then + TFSEC_VER="1.28.1" + safe_dl "https://github.com/aquasecurity/tfsec/releases/download/v${TFSEC_VER}/tfsec-linux-$(arch)" "$HOME/.local/bin/tfsec" + chmod +x "$HOME/.local/bin/tfsec" + ok "tfsec v${TFSEC_VER} installed" +fi + +if ! command -v terraform-docs > /dev/null 2>&1; then + TFD_VER="0.17.0" + safe_dl "https://github.com/terraform-docs/terraform-docs/releases/download/v${TFD_VER}/terraform-docs-v${TFD_VER}-linux-$(arch).tar.gz" "$SAFE_TMP/terraform-docs.tgz" + tar -xzf "$SAFE_TMP/terraform-docs.tgz" -C "$SAFE_TMP" + install -m 0755 "$SAFE_TMP/terraform-docs" "$HOME/.local/bin/terraform-docs" + ok "terraform-docs v${TFD_VER} installed" +fi + +if ! command -v pre-commit > /dev/null 2>&1; then + if command -v pip3 > /dev/null 2>&1; then + pip3 install --user --quiet pre-commit + ok "pre-commit installed" + elif command -v python3 > /dev/null 2>&1; then + python3 -m pip install --user --quiet pre-commit + ok "pre-commit installed" + else + warn "Python3/pip3 not found; skipping pre-commit" + fi +fi + +# ========================= +# Cloud CLIs (user-space) +# ========================= +echo +log "Installing Cloud CLIs (user-space)" + +# AWS CLI v2 +if [[ "${ENABLE_AWS}" == "true" ]] && ! command -v aws > /dev/null 2>&1; then + safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-$(arch).zip" "$SAFE_TMP/awscliv2.zip" \ + || safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" "$SAFE_TMP/awscliv2.zip" + unzip -q "$SAFE_TMP/awscliv2.zip" -d "$SAFE_TMP" + "$SAFE_TMP/aws/install" -i "$HOME/.local/aws-cli" -b "$HOME/.local/bin" > /dev/null + ok "AWS CLI installed" +fi + +# Azure CLI +if [[ "${ENABLE_AZURE}" == "true" ]] && ! command -v az > /dev/null 2>&1; then + if command -v pip3 > /dev/null 2>&1; then + pip3 install --user --quiet azure-cli && ok "Azure CLI installed" + elif command -v python3 > /dev/null 2>&1; then + python3 -m pip install --user --quiet azure-cli && ok "Azure CLI installed" + else + warn "Python/pip not found; cannot install Azure CLI" + fi +fi + +# Google Cloud SDK +if [[ "${ENABLE_GCP}" == "true" ]] && ! command -v gcloud > /dev/null 2>&1; then + GSDK_ARCH="$([[ "$(arch)" == amd64 ]] && echo x86_64 || echo arm)" + safe_dl "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-${GSDK_ARCH}.tar.gz" "$SAFE_TMP/gcloud.tgz" + tar -xzf "$SAFE_TMP/gcloud.tgz" -C "$HOME" + mv "$HOME/google-cloud-sdk" "$HOME/.local/google-cloud-sdk" + ln -sf "$HOME/.local/google-cloud-sdk/bin/"{gcloud,gsutil,bq} "$HOME/.local/bin/" || true + "$HOME/.local/google-cloud-sdk/install.sh" --quiet --rc-path /dev/null --path-update=false || true + ok "Google Cloud SDK installed" +fi + +# ========================= +# Container & K8s tools +# ========================= +echo +log "Installing container & Kubernetes tools" + +# Docker CLI (client only) +if ! command -v docker > /dev/null 2>&1; then + DOCKER_VER="25.0.5" + safe_dl "https://download.docker.com/linux/static/stable/$(docker_tar_arch)/docker-${DOCKER_VER}.tgz" "$SAFE_TMP/docker.tgz" + tar -xzf "$SAFE_TMP/docker.tgz" -C "$SAFE_TMP" + install -m 0755 "$SAFE_TMP/docker/docker" "$HOME/.local/bin/docker" + ok "Docker client installed" +fi + +# kubectl +if ! command -v kubectl > /dev/null 2>&1; then + KREL="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + safe_dl "https://dl.k8s.io/release/${KREL}/bin/linux/$(arch)/kubectl" "$SAFE_TMP/kubectl" + install -m 0755 "$SAFE_TMP/kubectl" "$HOME/.local/bin/kubectl" + ok "kubectl ${KREL} installed" +fi + +# Helm +if ! command -v helm > /dev/null 2>&1; then + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | USE_SUDO=false HELM_INSTALL_DIR="$HOME/.local/bin" bash + ok "Helm installed" +fi + +# jq / yq +if ! command -v jq > /dev/null 2>&1; then + safe_dl "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-$(arch)" "$HOME/.local/bin/jq" + chmod +x "$HOME/.local/bin/jq" + ok "jq installed" +fi + +if ! command -v yq > /dev/null 2>&1; then + safe_dl "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$(arch)" "$HOME/.local/bin/yq" + chmod +x "$HOME/.local/bin/yq" + ok "yq installed" +fi + +# ========================= +# Cloud runtime auth (optional) +# ========================= +echo +log "Configuring runtime cloud auth (if provided)" + +# AWS keys (override IRSA if present) +if [[ "${ENABLE_AWS}" == "true" ]] && [[ -n "$AWS_ACCESS_KEY_ID" ]]; then + mkdir -p "$HOME/.aws" + { + echo "[default]" + echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}" + echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}" + [[ -n "$AWS_SESSION_TOKEN" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}" + } > "$HOME/.aws/credentials" || { warn "Failed to write AWS credentials"; } + if [[ -n "$AWS_REGION" ]]; then + { + echo "[default]" + echo "region=${AWS_REGION}" + } > "$HOME/.aws/config" + fi + ok "AWS runtime creds configured${AWS_REGION:+ (region ${AWS_REGION})}" +else + skip "AWS runtime creds not set" +fi + +# Azure SP (client secret path; federated handled by helper) +if [[ "${ENABLE_AZURE}" == "true" ]] && [[ -n "$AZURE_CLIENT_ID" && -n "$AZURE_TENANT_ID" ]]; then + if command -v az > /dev/null 2>&1; then + if [[ -n "$AZURE_FEDERATED_TOKEN_FILE" && -f "$AZURE_FEDERATED_TOKEN_FILE" ]]; then + az login --service-principal --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat "$AZURE_FEDERATED_TOKEN_FILE")" \ + --allow-no-subscriptions > /dev/null + ok "Azure federated login complete" + elif [[ -n "$AZURE_CLIENT_SECRET" ]]; then + az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null + ok "Azure SP login complete" + else + skip "Azure creds not provided (need federated token file or client secret)" + fi + else + warn "Azure CLI not found; skipping login" + fi +else + skip "Azure runtime auth not configured" +fi + +# GCP SA JSON +if [[ "${ENABLE_GCP}" == "true" ]] && [[ -n "$GCP_SERVICE_ACCOUNT" ]]; then + if command -v gcloud > /dev/null 2>&1; then + echo "$GCP_SERVICE_ACCOUNT" > /tmp/gcp.json || { warn "Failed to write GCP credentials"; } + export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json + gcloud auth activate-service-account --key-file=/tmp/gcp.json > /dev/null || { warn "GCP auth failed"; } + [[ -n "$GCP_PROJECT_ID" ]] && gcloud config set project "$GCP_PROJECT_ID" --quiet || true + ok "GCP SA auth complete" + else + warn "gcloud not found; skipping GCP auth" + fi +else + skip "GCP runtime auth not configured" +fi + +# ========================= +# Git identity & bootstrap +# ========================= +echo +log "Preparing workspace directory" + +# Git identity +if [[ -n "$GIT_AUTHOR_NAME" ]]; then + git config --global user.name "$GIT_AUTHOR_NAME" +fi +if [[ -n "$GIT_AUTHOR_EMAIL" ]]; then + git config --global user.email "$GIT_AUTHOR_EMAIL" +fi + +mkdir -p "$WORKDIR" + +# Clone or init +if [[ -n "$REPO_URL" ]]; then + URL="$REPO_URL" + if [[ -n "$GITHUB_TOKEN" && "$URL" =~ ^https://github.com/ ]]; then + URL="${URL/https:\/\//https:\/\/${GITHUB_TOKEN}@}" || { warn "Failed to modify URL"; } + warn "Using GITHUB_TOKEN for private repo clone" + fi + if [[ ! -d "$WORKDIR/.git" ]]; then + log "Cloning ${REPO_URL} into ${WORKDIR}" + git clone "$URL" "$WORKDIR" || { warn "Failed to clone repository"; } + pushd "$WORKDIR" > /dev/null + git checkout "$DEFAULT_BRANCH" || git checkout -b "$DEFAULT_BRANCH" + popd > /dev/null + ok "Repository ready @ ${DEFAULT_BRANCH}" + else + ok "Repo already present at ${WORKDIR}" + fi +else + if [[ ! -d "$WORKDIR/.git" ]]; then + log "Initializing empty repository in ${WORKDIR}" + git init -q "$WORKDIR" + pushd "$WORKDIR" > /dev/null + git checkout -b "$DEFAULT_BRANCH" > /dev/null 2>&1 || true + popd > /dev/null + fi + ok "Workspace ready at ${WORKDIR}" +fi + +# ========================= +# Company Terraform skeleton +# ========================= +echo +log "Creating company Terraform skeleton (optional)" +mkdir -p "$WORKDIR/terraform"/{environments/{dev,staging,prod},modules,policies,shared} +cat > "$WORKDIR/terraform/README.md" << 'EOREADME' +# Company Terraform Project +- `environments/` contains per-env stacks. +- `modules/` reusable infra modules. +- `policies/` sentinel/policy-as-code. +- `shared/` backend, providers, etc. +EOREADME +ok "Skeleton present at $WORKDIR/terraform" + +# ========================= +# PATH persistence tip +# ========================= +if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2> /dev/null; then + echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc" +fi + +echo +ok "Workspace ready!" +echo " â€ĸ IaC tool: ${IAC_TOOL}" +echo " â€ĸ AWS enabled: ${ENABLE_AWS}" +echo " â€ĸ Azure enabled: ${ENABLE_AZURE}" +echo " â€ĸ GCP enabled: ${ENABLE_GCP}" +[[ -d "$WORKDIR/.git" ]] && echo " â€ĸ Repo: ${REPO_URL:-} @ ${DEFAULT_BRANCH}" +echo " â€ĸ Auth helpers: source ~/workspace/cloud-auth.sh" diff --git a/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl b/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl new file mode 100644 index 00000000..9009bbf4 --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/test/basic.tftest.hcl @@ -0,0 +1,87 @@ +# Run 'terraform test' from this template directory (where main.tf lives) + +# --- Mock cloud providers so no external calls happen --- +mock_provider "aws" {} +mock_provider "kubernetes" {} + +# Provide fake values for data sources your template reads +override_data { + target = data.aws_eks_cluster.eks + values = { + name = "unit-test-eks" + endpoint = "https://example.eks.local" + certificate_authority = [{ + data = base64encode("dummy-ca") + }] + } +} + +override_data { + target = data.aws_eks_cluster_auth.eks + values = { + token = "dummy-token" + } +} + +# --------------------------- +# 1) Validate configuration +# --------------------------- +run "validate" { + command = validate +} + +# --------------------------- +# 2) Plan with representative inputs +# --------------------------- +run "plan_with_defaults" { + command = plan + + variables { + host_cluster_name = "unit-test-eks" + + # IaC/tooling toggles + iac_tool = "terraform" + enable_aws = true + enable_azure = false + enable_gcp = false + + # Dev creds (empty OK for unit test) + aws_access_key_id = "" + aws_secret_access_key = "" + azure_client_id = "" + azure_tenant_id = "" + azure_client_secret = "" + gcp_service_account = "" + } + + # Simple sanity assertions (adjust resource addresses to your template) + assert { + condition = can(resource.kubernetes_namespace.workspace) + error_message = "kubernetes_namespace.workspace was not created in plan." + } + + assert { + condition = can(resource.coder_agent.main) + error_message = "coder_agent.main was not planned." + } +} + +# --------------------------- +# 3) Plan with CDK selected +# --------------------------- +run "plan_with_cdk" { + command = plan + variables { + host_cluster_name = "unit-test-eks" + iac_tool = "cdk" + enable_aws = true + enable_azure = false + enable_gcp = false + } + + # Ensure the env reflects choice (string map lookup) + assert { + condition = contains(keys(resource.coder_agent.main.env), "IAC_TOOL") + error_message = "IAC_TOOL env not present on coder_agent.main." + } +} diff --git a/registry/nboyers/templates/cloud-dev/variables.tf b/registry/nboyers/templates/cloud-dev/variables.tf new file mode 100644 index 00000000..5fce229b --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/variables.tf @@ -0,0 +1,120 @@ +# --- Host cluster (where the workspace runs) --- +variable "host_cluster_name" { + description = "EKS cluster name" + type = string + + validation { + condition = can(regex("^[0-9A-Za-z][0-9A-Za-z_-]*$", trimspace(var.host_cluster_name))) + error_message = "Cluster name must match ^[0-9A-Za-z][0-9A-Za-z_-]*$ (no leading space)." + } +} + + +# --- Admin: IaC tool & toggles --- +variable "iac_tool" { + description = "Infrastructure as Code tool" + type = string + default = "terraform" + validation { + condition = contains(["terraform", "cdk", "pulumi"], var.iac_tool) + error_message = "Must be one of: terraform, cdk, pulumi" + } +} + + +variable "enable_aws" { + type = bool + default = true +} + +variable "enable_azure" { + type = bool + default = false +} + +variable "enable_gcp" { + type = bool + default = false +} + +# --- AWS --- +variable "aws_region" { + type = string + default = "us-west-2" +} + +variable "aws_role_arn" { + type = string + default = "" # IRSA optional +} + +variable "aws_access_key_id" { + type = string + default = "" + sensitive = true +} + +variable "aws_secret_access_key" { + type = string + default = "" + sensitive = true +} + +variable "aws_session_token" { + description = "Optional STS session token" + type = string + default = "" + sensitive = true +} + +variable "repo_url" { + description = "Git repository to clone into the workspace (optional)" + type = string + default = "" +} + +variable "default_branch" { + description = "Default branch name to use (if repo is empty or for initial checkout)" + type = string + default = "main" +} + + +# --- Azure --- +variable "azure_subscription_id" { + type = string + default = "" +} + +variable "azure_tenant_id" { + type = string + default = "" + sensitive = true +} + +variable "azure_client_id" { + type = string + default = "" + sensitive = true +} + +variable "azure_client_secret" { + type = string + default = "" + sensitive = true +} + +# --- GCP --- +variable "gcp_project_id" { + type = string + default = "" +} + +variable "gcp_service_account" { + description = "Service Account JSON (paste full JSON) — leave empty if using WIF" + type = string + default = "" + sensitive = true +} + +