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 @@
+
+
\ 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
+}
+
+