diff --git a/.github/scripts/version-bump.sh b/.github/scripts/version-bump.sh index f84a7f89..ec078fcc 100755 --- a/.github/scripts/version-bump.sh +++ b/.github/scripts/version-bump.sh @@ -1,14 +1,18 @@ #!/bin/bash # Version Bump Script -# Usage: ./version-bump.sh [base_ref] +# Usage: ./version-bump.sh [--ci] [base_ref] +# --ci: CI mode - run bump, check for changes, exit 1 if changes needed # bump_type: patch, minor, or major # base_ref: base reference for diff (default: origin/main) set -euo pipefail +CI_MODE=false + usage() { - echo "Usage: $0 [base_ref]" + echo "Usage: $0 [--ci] [base_ref]" + echo " --ci: CI mode - validates versions are already bumped (exits 1 if not)" echo " bump_type: patch, minor, or major" echo " base_ref: base reference for diff (default: origin/main)" echo "" @@ -16,6 +20,7 @@ usage() { echo " $0 patch # Update versions with patch bump" echo " $0 minor # Update versions with minor bump" echo " $0 major # Update versions with major bump" + echo " $0 --ci patch # CI check: verify patch bump has been applied" exit 1 } @@ -85,7 +90,7 @@ update_readme_version() { in_module_block = 0 if (module_has_target_source) { num_lines = split(module_content, lines, "\n") - for (i = 1; i <= num_lines; i++) { + for (i = 1; i < num_lines; i++) { line = lines[i] if (line ~ /^[[:space:]]*version[[:space:]]*=/) { match(line, /^[[:space:]]*/) @@ -115,6 +120,11 @@ update_readme_version() { } main() { + if [ "${1:-}" = "--ci" ]; then + CI_MODE=true + shift + fi + if [ $# -lt 1 ] || [ $# -gt 2 ]; then usage fi @@ -152,6 +162,8 @@ main() { local untagged_modules="" local has_changes=false + declare -a modified_readme_files=() + while IFS= read -r module_path; do if [ -z "$module_path" ]; then continue; fi @@ -202,6 +214,7 @@ main() { if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then updated_readmes="$updated_readmes\n- $namespace/$module_name" + modified_readme_files+=("$readme_path") has_changes=true fi @@ -210,19 +223,22 @@ main() { done <<< "$modules" - # Always run formatter to ensure consistent formatting - echo "🔧 Running formatter to ensure consistent formatting..." - if command -v bun > /dev/null 2>&1; then - bun fmt > /dev/null 2>&1 || echo "âš ī¸ Warning: bun fmt failed, but continuing..." - else - echo "âš ī¸ Warning: bun not found, skipping formatting" + if [ ${#modified_readme_files[@]} -gt 0 ]; then + echo "🔧 Formatting modified README files..." + if command -v bun > /dev/null 2>&1; then + for readme_file in "${modified_readme_files[@]}"; do + bun run prettier --write "$readme_file" 2> /dev/null || true + done + else + echo "âš ī¸ Warning: bun not found, skipping formatting" + fi + echo "" fi - echo "" echo "📋 Summary:" echo "Bump Type: $bump_type" echo "" - echo "Modules Updated:" + echo "Modules Processed:" echo -e "$bumped_modules" echo "" @@ -239,6 +255,19 @@ main() { echo "" fi + if [ "$CI_MODE" = true ]; then + echo "🔍 Comparing files to committed versions..." + if git diff --quiet; then + echo "✅ PASS: All versions match - no changes needed" + exit 0 + else + echo "❌ FAIL: Module versions need to be updated" + echo "" + echo "Run './.github/scripts/version-bump.sh $bump_type' locally and commit the changes" + exit 1 + fi + fi + if [ "$has_changes" = true ]; then echo "✅ Version bump completed successfully!" echo "📝 README files have been updated with new versions." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5601ef6a..bc16a5b8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,7 +93,7 @@ jobs: - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@v1.40.0 + uses: crate-ci/typos@v1.41.0 with: config: .github/typos.toml validate-readme-files: diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml index 7c51d0ef..aff9e0a1 100644 --- a/.github/workflows/version-bump.yaml +++ b/.github/workflows/version-bump.yaml @@ -55,62 +55,35 @@ jobs: ;; esac - - name: Check version bump requirements - id: version-check - run: | - output_file=$(mktemp) - if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then - echo "Script completed successfully" - else - echo "Script failed" - cat "$output_file" - exit 1 - fi + - name: Check version bump + run: ./.github/scripts/version-bump.sh --ci "${{ steps.bump-type.outputs.type }}" origin/main - { - echo "output<> $GITHUB_OUTPUT - - cat "$output_file" - - if git diff --quiet; then - echo "versions_up_to_date=true" >> $GITHUB_OUTPUT - echo "✅ All module versions are already up to date" - else - echo "versions_up_to_date=false" >> $GITHUB_OUTPUT - echo "❌ Module versions need to be updated" - echo "Files that would be changed:" - git diff --name-only - echo "" - echo "Diff preview:" - git diff - - git checkout . - git clean -fd - - exit 1 - fi - - - name: Comment on PR - Failure - if: failure() && steps.version-check.outputs.versions_up_to_date == 'false' + - name: Comment on PR - Version bump required + if: failure() uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const output = `${{ steps.version-check.outputs.output }}`; const bumpType = `${{ steps.bump-type.outputs.type }}`; - let comment = `## ❌ Version Bump Validation Failed\n\n`; - comment += `**Bump Type:** \`${bumpType}\`\n\n`; - comment += `Module versions need to be updated but haven't been bumped yet.\n\n`; - comment += `**Required Actions:**\n`; - comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`; - comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`; - comment += `3. Push the changes: \`git push\`\n\n`; - comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`; - comment += `> Please update the module versions and push the changes to continue.`; + const comment = [ + '## Version Bump Required', + '', + 'One or more modules in this PR need their versions updated.', + '', + '**To fix this:**', + '1. Run the version bump script locally:', + ' ```bash', + ` ./.github/scripts/version-bump.sh ${bumpType}`, + ' ```', + '2. Commit the changes:', + ' ```bash', + ` git add . && git commit -m "chore: bump module versions (${bumpType})"`, + ' ```', + '3. Push your changes', + '', + 'The CI will automatically re-run once you push the updated versions.' + ].join('\n'); github.rest.issues.createComment({ issue_number: context.issue.number, 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/cloud-devops.svg b/.icons/cloud-devops.svg new file mode 100644 index 00000000..a3b6e82a --- /dev/null +++ b/.icons/cloud-devops.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/AJ0070/.images/avatar.jpeg b/registry/AJ0070/.images/avatar.jpeg index d47bfb80..568decd2 100644 Binary files a/registry/AJ0070/.images/avatar.jpeg and b/registry/AJ0070/.images/avatar.jpeg differ diff --git a/registry/AJ0070/.images/avatar.png b/registry/AJ0070/.images/avatar.png index 8f0ff917..c62ffc9d 100644 Binary files a/registry/AJ0070/.images/avatar.png and b/registry/AJ0070/.images/avatar.png differ diff --git a/registry/Excellencedev/.images/avatar.png b/registry/Excellencedev/.images/avatar.png index 4ca6333f..e883c2cf 100644 Binary files a/registry/Excellencedev/.images/avatar.png and b/registry/Excellencedev/.images/avatar.png differ diff --git a/registry/anomaly/.images/avatar.jpeg b/registry/anomaly/.images/avatar.jpeg index ca1072d8..f823960a 100644 Binary files a/registry/anomaly/.images/avatar.jpeg and b/registry/anomaly/.images/avatar.jpeg differ 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-labs/.images/openwebui.png b/registry/coder-labs/.images/openwebui.png index 709d3e5f..5f2c3fce 100644 Binary files a/registry/coder-labs/.images/openwebui.png and b/registry/coder-labs/.images/openwebui.png differ diff --git a/registry/coder-labs/.images/perplexica.png b/registry/coder-labs/.images/perplexica.png index 84084937..9bb6ea28 100644 Binary files a/registry/coder-labs/.images/perplexica.png and b/registry/coder-labs/.images/perplexica.png differ diff --git a/registry/coder-labs/modules/auggie/README.md b/registry/coder-labs/modules/auggie/README.md index fd1632fe..562bcbd4 100644 --- a/registry/coder-labs/modules/auggie/README.md +++ b/registry/coder-labs/modules/auggie/README.md @@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad ```tf module "auggie" { source = "registry.coder.com/coder-labs/auggie/coder" - version = "0.2.2" + version = "0.3.0" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -47,7 +47,7 @@ module "coder-login" { module "auggie" { source = "registry.coder.com/coder-labs/auggie/coder" - version = "0.2.2" + version = "0.3.0" agent_id = coder_agent.example.id folder = "/home/coder/project" @@ -103,7 +103,7 @@ EOF ```tf module "auggie" { source = "registry.coder.com/coder-labs/auggie/coder" - version = "0.2.2" + version = "0.3.0" agent_id = coder_agent.example.id folder = "/home/coder/project" diff --git a/registry/coder-labs/modules/auggie/main.tf b/registry/coder-labs/modules/auggie/main.tf index d75b1229..8ecb3ba0 100644 --- a/registry/coder-labs/modules/auggie/main.tf +++ b/registry/coder-labs/modules/auggie/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -179,7 +179,7 @@ locals { module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.folder @@ -229,4 +229,8 @@ module "agentapi" { ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \ /tmp/install.sh EOT -} \ No newline at end of file +} + +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md index 4ef2f6bc..76b8f025 100644 --- a/registry/coder-labs/modules/copilot/README.md +++ b/registry/coder-labs/modules/copilot/README.md @@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.3" + version = "0.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/projects" } @@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.3" + version = "0.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/projects" @@ -71,7 +71,7 @@ Customize tool permissions, MCP servers, and Copilot settings: ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.3" + version = "0.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/projects" @@ -142,7 +142,7 @@ variable "github_token" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.3" + version = "0.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/projects" github_token = var.github_token @@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.3" + version = "0.3.0" agent_id = coder_agent.example.id workdir = "/home/coder" report_tasks = false diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf index 41a83d53..218184d7 100644 --- a/registry/coder-labs/modules/copilot/main.tf +++ b/registry/coder-labs/modules/copilot/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -242,7 +242,7 @@ resource "coder_env" "github_token" { module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.workdir @@ -268,7 +268,7 @@ module "agentapi" { set -o pipefail echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh chmod +x /tmp/start.sh - + ARG_WORKDIR='${local.workdir}' \ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \ @@ -288,7 +288,7 @@ module "agentapi" { set -o pipefail echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh chmod +x /tmp/install.sh - + ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_WORKDIR='${local.workdir}' \ @@ -299,4 +299,8 @@ module "agentapi" { ARG_COPILOT_MODEL='${var.copilot_model}' \ /tmp/install.sh EOT -} \ No newline at end of file +} + +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder-labs/modules/cursor-cli/README.md b/registry/coder-labs/modules/cursor-cli/README.md index 6aa5ada3..6b710530 100644 --- a/registry/coder-labs/modules/cursor-cli/README.md +++ b/registry/coder-labs/modules/cursor-cli/README.md @@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and ```tf module "cursor_cli" { source = "registry.coder.com/coder-labs/cursor-cli/coder" - version = "0.2.2" + version = "0.3.0" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -42,7 +42,7 @@ module "coder-login" { module "cursor_cli" { source = "registry.coder.com/coder-labs/cursor-cli/coder" - version = "0.2.2" + version = "0.3.0" agent_id = coder_agent.main.id folder = "/home/coder/project" diff --git a/registry/coder-labs/modules/cursor-cli/main.test.ts b/registry/coder-labs/modules/cursor-cli/main.test.ts index 4b37b71c..b7f9947c 100644 --- a/registry/coder-labs/modules/cursor-cli/main.test.ts +++ b/registry/coder-labs/modules/cursor-cli/main.test.ts @@ -159,7 +159,7 @@ describe("cursor-cli", async () => { "-c", "cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true", ]); - expect(startLog.stdout).toContain(`-m ${model}`); + expect(startLog.stdout).toContain(`--model ${model}`); expect(startLog.stdout).toContain("-f"); expect(startLog.stdout).toContain("test prompt"); }); diff --git a/registry/coder-labs/modules/cursor-cli/main.tf b/registry/coder-labs/modules/cursor-cli/main.tf index a57ad65c..4363f358 100644 --- a/registry/coder-labs/modules/cursor-cli/main.tf +++ b/registry/coder-labs/modules/cursor-cli/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" { module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.folder @@ -179,3 +179,7 @@ module "agentapi" { /tmp/install.sh EOT } + +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder-labs/modules/cursor-cli/scripts/start.sh b/registry/coder-labs/modules/cursor-cli/scripts/start.sh index 8a200a2f..1e9ab59d 100644 --- a/registry/coder-labs/modules/cursor-cli/scripts/start.sh +++ b/registry/coder-labs/modules/cursor-cli/scripts/start.sh @@ -50,7 +50,7 @@ ARGS=() # global flags if [ -n "$ARG_MODEL" ]; then - ARGS+=("-m" "$ARG_MODEL") + ARGS+=("--model" "$ARG_MODEL") fi if [ "$ARG_FORCE" = "true" ]; then ARGS+=("-f") diff --git a/registry/coder-labs/modules/gemini/README.md b/registry/coder-labs/modules/gemini/README.md index 8e44c89b..d0a113a0 100644 --- a/registry/coder-labs/modules/gemini/README.md +++ b/registry/coder-labs/modules/gemini/README.md @@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace ```tf module "gemini" { source = "registry.coder.com/coder-labs/gemini/coder" - version = "2.1.2" + version = "3.0.0" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -46,7 +46,7 @@ variable "gemini_api_key" { module "gemini" { source = "registry.coder.com/coder-labs/gemini/coder" - version = "2.1.2" + version = "3.0.0" agent_id = coder_agent.main.id gemini_api_key = var.gemini_api_key folder = "/home/coder/project" @@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" { module "gemini" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/gemini/coder" - version = "2.1.2" + version = "3.0.0" agent_id = coder_agent.main.id gemini_api_key = var.gemini_api_key gemini_model = "gemini-2.5-flash" @@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform: ```tf module "gemini" { source = "registry.coder.com/coder-labs/gemini/coder" - version = "2.1.2" + version = "3.0.0" agent_id = coder_agent.main.id gemini_api_key = var.gemini_api_key folder = "/home/coder/project" diff --git a/registry/coder-labs/modules/gemini/main.tf b/registry/coder-labs/modules/gemini/main.tf index 7cc8ac04..dbc81bc7 100644 --- a/registry/coder-labs/modules/gemini/main.tf +++ b/registry/coder-labs/modules/gemini/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -177,7 +177,7 @@ EOT module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.folder @@ -225,4 +225,8 @@ module "agentapi" { GEMINI_TASK_PROMPT='${var.task_prompt}' \ /tmp/start.sh EOT -} \ No newline at end of file +} + +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md index 6ba0576f..0fabafe8 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/README.md +++ b/registry/coder-labs/modules/sourcegraph-amp/README.md @@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI ```tf module "amp-cli" { source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - version = "2.1.0" + version = "3.0.0" agent_id = coder_agent.example.id amp_api_key = var.amp_api_key install_amp = true @@ -48,7 +48,7 @@ variable "amp_api_key" { module "amp-cli" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - amp_version = "2.1.0" + amp_version = "3.0.0" agent_id = coder_agent.example.id amp_api_key = var.amp_api_key # recommended for tasks usage workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.test.ts b/registry/coder-labs/modules/sourcegraph-amp/main.test.ts index dc5328a3..105f7386 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.test.ts +++ b/registry/coder-labs/modules/sourcegraph-amp/main.test.ts @@ -110,6 +110,7 @@ describe("amp", async () => { const { id } = await setup({ skipAmpMock: true, moduleVariables: { + install_via_npm: "true", amp_version: "0.0.1755964909-g31e083", }, }); diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf index 31433795..13b25ec0 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.tf +++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } external = { source = "hashicorp/external" @@ -140,7 +140,7 @@ variable "base_amp_config" { type = string description = <<-EOT Base AMP configuration in JSON format. Can be overridden to customize AMP settings. - + If empty, defaults enable thinking and todos for autonomous operation. Additional options include: - "amp.permissions": [] (tool permissions) - "amp.tools.stopTimeout": 600 (extend timeout for long operations) @@ -148,7 +148,7 @@ variable "base_amp_config" { - "amp.tools.disable": ["builtin:open"] (disable tools for containers) - "amp.git.commit.ampThread.enabled": true (link commits to threads) - "amp.git.commit.coauthor.enabled": true (add Amp as co-author) - + Reference: https://ampcode.com/manual EOT default = "" @@ -220,7 +220,7 @@ locals { module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.workdir @@ -268,4 +268,6 @@ module "agentapi" { EOT } - +output "task_app_id" { + value = module.agentapi.task_app_id +} diff --git a/registry/coder/.images/amazon-q.png b/registry/coder/.images/amazon-q.png index 1221cc8b..eae79efc 100644 Binary files a/registry/coder/.images/amazon-q.png and b/registry/coder/.images/amazon-q.png differ diff --git a/registry/coder/.images/rstudio-server.png b/registry/coder/.images/rstudio-server.png index 71ecc464..a75e76c6 100644 Binary files a/registry/coder/.images/rstudio-server.png and b/registry/coder/.images/rstudio-server.png differ diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 11172010..0256e029 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi ```tf module "claude-code" { source = "dev.registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" enable_boundary = true @@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" @@ -108,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" install_claude_code = true @@ -130,7 +130,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -203,7 +203,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -260,7 +260,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.2.7" + version = "4.2.8" agent_id = coder_agent.main.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 8623336d..1e4754d9 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -86,7 +86,7 @@ variable "install_agentapi" { variable "agentapi_version" { type = string description = "The version of AgentAPI to install." - default = "v0.11.4" + default = "v0.11.6" } variable "ai_prompt" { diff --git a/registry/coder/modules/code-server/README.md b/registry/coder/modules/code-server/README.md index fca18909..3312f979 100644 --- a/registry/coder/modules/code-server/README.md +++ b/registry/coder/modules/code-server/README.md @@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id } ``` @@ -29,9 +29,9 @@ module "code-server" { module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id - install_version = "1.4.1" + install_version = "4.106.3" } ``` @@ -43,7 +43,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/): module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id extensions = [ "dracula-theme.theme-dracula" @@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } @@ -92,7 +92,7 @@ You can pass additional command-line arguments to code-server using the `additio module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id additional_args = "--disable-workspace-trust" } @@ -108,7 +108,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -121,8 +121,10 @@ Just run code-server in the background, don't fetch it from GitHub: module "code-server" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/code-server/coder" - version = "1.4.1" + version = "1.4.2" agent_id = coder_agent.example.id offline = true } ``` + +Some of the key differences between code-server and [VS Code Web](https://registry.coder.com/modules/coder/vscode-web) are listed in [docs](https://coder.com/docs/user-guides/workspace-access/code-server#differences-between-code-server-and-vs-code-web). 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/github-upload-public-key/main.test.ts b/registry/coder/modules/github-upload-public-key/main.test.ts index 467d6b95..6d8aae27 100644 --- a/registry/coder/modules/github-upload-public-key/main.test.ts +++ b/registry/coder/modules/github-upload-public-key/main.test.ts @@ -1,9 +1,17 @@ -import { type Server, serve } from "bun"; -import { describe, expect, it } from "bun:test"; +import { serve } from "bun"; +import { + afterEach, + beforeAll, + describe, + expect, + it, + setDefaultTimeout, +} from "bun:test"; import { createJSONResponse, execContainer, findResourceInstance, + removeContainer, runContainer, runTerraformApply, runTerraformInit, @@ -11,77 +19,48 @@ import { writeCoder, } from "~test"; -describe("github-upload-public-key", async () => { - await runTerraformInit(import.meta.dir); - - testRequiredVariables(import.meta.dir, { - agent_id: "foo", - }); - - it("creates new key if one does not exist", async () => { - const { instance, id, server } = await setupContainer(); - await writeCoder(id, "echo foo"); - - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, - ]); - expect(exec.stdout).toContain( - "Your Coder public key has been added to GitHub!", - ); - expect(exec.exitCode).toBe(0); - // we need to increase timeout to pull the container - }, 15000); - - it("does nothing if one already exists", async () => { - const { instance, id, server } = await setupContainer(); - // use keyword to make server return a existing key - await writeCoder(id, "echo findkey"); - - const url = server.url.toString().slice(0, -1); - const exec = await execContainer(id, [ - "env", - `CODER_ACCESS_URL=${url}`, - `GITHUB_API_URL=${url}`, - "CODER_OWNER_SESSION_TOKEN=foo", - "CODER_EXTERNAL_AUTH_ID=github", - "bash", - "-c", - instance.script, - ]); - expect(exec.stdout).toContain( - "Your Coder public key is already on GitHub!", - ); - expect(exec.exitCode).toBe(0); - }); +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); +}; +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } }); const setupContainer = async ( image = "lorello/alpine-bash", vars: Record = {}, ) => { - const server = await setupServer(); + const server = setupServer(); const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", ...vars, }); const instance = findResourceInstance(state, "coder_script"); const id = await runContainer(image); + + registerCleanup(async () => { + server.stop(); + }); + registerCleanup(async () => { + await removeContainer(id); + }); + return { id, instance, server }; }; -const setupServer = async (): Promise => { - let url: URL; - const fakeSlackHost = serve({ +const setupServer = () => { + const fakeGithubHost = serve({ fetch: (req) => { - url = new URL(req.url); + const url = new URL(req.url); if (url.pathname === "/api/v2/users/me/gitsshkey") { return createJSONResponse({ public_key: "exists", @@ -128,5 +107,60 @@ const setupServer = async (): Promise => { port: 0, }); - return fakeSlackHost; + return fakeGithubHost; }; + +setDefaultTimeout(30 * 1000); + +describe("github-upload-public-key", () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + }); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("creates new key if one does not exist", async () => { + const { instance, id, server } = await setupContainer(); + await writeCoder(id, "echo foo"); + + const url = server.url.toString().slice(0, -1); + const exec = await execContainer(id, [ + "env", + `CODER_ACCESS_URL=${url}`, + `GITHUB_API_URL=${url}`, + "CODER_OWNER_SESSION_TOKEN=foo", + "CODER_EXTERNAL_AUTH_ID=github", + "bash", + "-c", + instance.script, + ]); + expect(exec.stdout).toContain( + "Your Coder public key has been added to GitHub!", + ); + expect(exec.exitCode).toBe(0); + }); + + it("does nothing if one already exists", async () => { + const { instance, id, server } = await setupContainer(); + // use keyword to make server return a existing key + await writeCoder(id, "echo findkey"); + + const url = server.url.toString().slice(0, -1); + const exec = await execContainer(id, [ + "env", + `CODER_ACCESS_URL=${url}`, + `GITHUB_API_URL=${url}`, + "CODER_OWNER_SESSION_TOKEN=foo", + "CODER_EXTERNAL_AUTH_ID=github", + "bash", + "-c", + instance.script, + ]); + expect(exec.stdout).toContain( + "Your Coder public key is already on GitHub!", + ); + expect(exec.exitCode).toBe(0); + }); +}); 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/coder/modules/vault-cli/README.md b/registry/coder/modules/vault-cli/README.md index f8df790f..04420570 100644 --- a/registry/coder/modules/vault-cli/README.md +++ b/registry/coder/modules/vault-cli/README.md @@ -13,7 +13,7 @@ Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" } @@ -34,7 +34,7 @@ If you have a Vault token, you can provide it to automatically configure authent ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_token = var.vault_token # Optional @@ -50,7 +50,7 @@ Install the Vault CLI without any authentication: ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" } @@ -61,7 +61,7 @@ module "vault_cli" { ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_cli_version = "1.15.0" @@ -73,7 +73,7 @@ module "vault_cli" { ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" install_dir = "/home/coder/bin" @@ -87,7 +87,7 @@ For Vault Enterprise users who need to specify a namespace: ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_token = var.vault_token @@ -102,7 +102,7 @@ Install the Vault Enterprise binary. This is required if using SAML authenticati ```tf module "vault_cli" { source = "registry.coder.com/coder/vault-cli/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" enterprise = true diff --git a/registry/coder/modules/vault-cli/run.sh b/registry/coder/modules/vault-cli/run.sh index 18803ee5..40c2c26d 100644 --- a/registry/coder/modules/vault-cli/run.sh +++ b/registry/coder/modules/vault-cli/run.sh @@ -7,40 +7,34 @@ INSTALL_DIR=${INSTALL_DIR} VAULT_CLI_VERSION=${VAULT_CLI_VERSION} ENTERPRISE=${ENTERPRISE} -# Fetch URL content. If dest is provided, write to file; otherwise output to stdout. -# Usage: fetch [dest] +# Fetch URL content to stdout fetch() { url="$1" - dest="$${2:-}" - - # Detect HTTP client on first run - if [ -z "$${HTTP_CLIENT:-}" ]; then - if command -v curl > /dev/null 2>&1; then - HTTP_CLIENT="curl" - elif command -v wget > /dev/null 2>&1; then - HTTP_CLIENT="wget" - elif command -v busybox > /dev/null 2>&1; then - HTTP_CLIENT="busybox" - else - printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n" - return 1 - fi - fi - - if [ -n "$${dest}" ]; then - # shellcheck disable=SC2195 - case "$${HTTP_CLIENT}" in - curl) curl -sSL --fail "$${url}" -o "$${dest}" ;; - wget) wget -O "$${dest}" "$${url}" ;; - busybox) busybox wget -O "$${dest}" "$${url}" ;; - esac + if command -v curl > /dev/null 2>&1; then + curl -sSL --fail "$${url}" + elif command -v wget > /dev/null 2>&1; then + wget -qO- "$${url}" + elif command -v busybox > /dev/null 2>&1; then + busybox wget -qO- "$${url}" else - # shellcheck disable=SC2195 - case "$${HTTP_CLIENT}" in - curl) curl -sSL --fail "$${url}" ;; - wget) wget -qO- "$${url}" ;; - busybox) busybox wget -qO- "$${url}" ;; - esac + printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n" + return 1 + fi +} + +# Download URL to a file +fetch_to_file() { + dest="$1" + url="$2" + if command -v curl > /dev/null 2>&1; then + curl -sSL --fail "$${url}" -o "$${dest}" + elif command -v wget > /dev/null 2>&1; then + wget -O "$${dest}" "$${url}" + elif command -v busybox > /dev/null 2>&1; then + busybox wget -O "$${dest}" "$${url}" + else + printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n" + return 1 fi } @@ -141,7 +135,7 @@ install() { cd "$${TEMP_DIR}" || return 1 printf "Downloading from %s\n" "$${DOWNLOAD_URL}" - if ! fetch "$${DOWNLOAD_URL}" vault.zip; then + if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then printf "Failed to download Vault.\n" rm -rf "$${TEMP_DIR}" return 1 diff --git a/registry/coder/modules/zed/README.md b/registry/coder/modules/zed/README.md index 60e72f6c..cffb7144 100644 --- a/registry/coder/modules/zed/README.md +++ b/registry/coder/modules/zed/README.md @@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id } ``` @@ -32,7 +32,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -44,7 +44,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id display_name = "Zed Editor" order = 1 @@ -57,7 +57,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id agent_name = coder_agent.example.name } @@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.3" + version = "1.1.4" agent_id = coder_agent.main.id settings = jsonencode({ @@ -85,6 +85,7 @@ module "zed" { env = {} } + } }) } diff --git a/registry/coder/modules/zed/main.test.ts b/registry/coder/modules/zed/main.test.ts index 12413750..9a39b47b 100644 --- a/registry/coder/modules/zed/main.test.ts +++ b/registry/coder/modules/zed/main.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "bun:test"; import { + execContainer, + findResourceInstance, + removeContainer, + runContainer, runTerraformApply, runTerraformInit, testRequiredVariables, @@ -12,66 +16,114 @@ describe("zed", async () => { agent_id: "foo", }); - it("default output", async () => { + it("creates settings file with correct JSON", async () => { + const settings = { + theme: "One Dark", + buffer_font_size: 14, + vim_mode: true, + telemetry: { + diagnostics: false, + metrics: false, + }, + // Test special characters: single quotes, backslashes, URLs + message: "it's working", + path: "C:\\Users\\test", + api_url: "https://api.example.com/v1?token=abc&user=test", + }; + const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", + settings: JSON.stringify(settings), }); - expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder"); - const coder_app = state.resources.find( - (res) => res.type === "coder_app" && res.name === "zed", - ); + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer("alpine:latest"); - expect(coder_app).not.toBeNull(); - expect(coder_app?.instances.length).toBe(1); - expect(coder_app?.instances[0].attributes.order).toBeNull(); - }); + try { + const result = await execContainer(id, ["sh", "-c", instance.script]); + expect(result.exitCode).toBe(0); + + const catResult = await execContainer(id, [ + "cat", + "/root/.config/zed/settings.json", + ]); + expect(catResult.exitCode).toBe(0); + + const written = JSON.parse(catResult.stdout.trim()); + expect(written).toEqual(settings); + } finally { + await removeContainer(id); + } + }, 30000); + + it("merges settings with existing file when jq available", async () => { + const existingSettings = { + theme: "Solarized Dark", + vim_mode: true, + }; + + const newSettings = { + theme: "One Dark", + buffer_font_size: 14, + }; - it("adds folder", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - folder: "/foo/bar", + settings: JSON.stringify(newSettings), }); - expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder/foo/bar"); - }); - it("expect order to be set", async () => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer("alpine:latest"); + + try { + // Install jq and create existing settings file + await execContainer(id, ["apk", "add", "--no-cache", "jq"]); + await execContainer(id, ["mkdir", "-p", "/root/.config/zed"]); + await execContainer(id, [ + "sh", + "-c", + `echo '${JSON.stringify(existingSettings)}' > /root/.config/zed/settings.json`, + ]); + + const result = await execContainer(id, ["sh", "-c", instance.script]); + expect(result.exitCode).toBe(0); + + const catResult = await execContainer(id, [ + "cat", + "/root/.config/zed/settings.json", + ]); + expect(catResult.exitCode).toBe(0); + + const merged = JSON.parse(catResult.stdout.trim()); + expect(merged.theme).toBe("One Dark"); // overwritten + expect(merged.buffer_font_size).toBe(14); // added + expect(merged.vim_mode).toBe(true); // preserved + } finally { + await removeContainer(id); + } + }, 30000); + + it("exits early with empty settings", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - order: "22", + settings: "", }); - const coder_app = state.resources.find( - (res) => res.type === "coder_app" && res.name === "zed", - ); + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer("alpine:latest"); - expect(coder_app).not.toBeNull(); - expect(coder_app?.instances.length).toBe(1); - expect(coder_app?.instances[0].attributes.order).toBe(22); - }); + try { + const result = await execContainer(id, ["sh", "-c", instance.script]); + expect(result.exitCode).toBe(0); - it("expect display_name to be set", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - display_name: "Custom Zed", - }); - - const coder_app = state.resources.find( - (res) => res.type === "coder_app" && res.name === "zed", - ); - - expect(coder_app).not.toBeNull(); - expect(coder_app?.instances.length).toBe(1); - expect(coder_app?.instances[0].attributes.display_name).toBe("Custom Zed"); - }); - - it("adds agent_name to hostname", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - agent_name: "myagent", - }); - expect(state.outputs.zed_url.value).toBe( - "zed://ssh/myagent.default.default.coder", - ); - }); + // Settings file should not be created + const catResult = await execContainer(id, [ + "cat", + "/root/.config/zed/settings.json", + ]); + expect(catResult.exitCode).not.toBe(0); + } finally { + await removeContainer(id); + } + }, 30000); }); diff --git a/registry/coder/modules/zed/main.tf b/registry/coder/modules/zed/main.tf index 745254be..414c3c0e 100644 --- a/registry/coder/modules/zed/main.tf +++ b/registry/coder/modules/zed/main.tf @@ -65,6 +65,7 @@ locals { owner_name = lower(data.coder_workspace_owner.me.name) agent_name = lower(var.agent_name) hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder" + settings_b64 = var.settings != "" ? base64encode(var.settings) : "" } resource "coder_script" "zed_settings" { @@ -75,7 +76,11 @@ resource "coder_script" "zed_settings" { script = <<-EOT #!/usr/bin/env bash set -eu - SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}' + SETTINGS_B64='${local.settings_b64}' + if [ -z "$${SETTINGS_B64}" ]; then + exit 0 + fi + SETTINGS_JSON="$(echo -n "$${SETTINGS_B64}" | base64 -d)" if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then exit 0 fi diff --git a/registry/coder/modules/zed/zed.tftest.hcl b/registry/coder/modules/zed/zed.tftest.hcl index 508b6550..339f6876 100644 --- a/registry/coder/modules/zed/zed.tftest.hcl +++ b/registry/coder/modules/zed/zed.tftest.hcl @@ -5,6 +5,20 @@ run "default_output" { agent_id = "foo" } + override_data { + target = data.coder_workspace.me + values = { + name = "default" + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + name = "default" + } + } + assert { condition = output.zed_url == "zed://ssh/default.coder" error_message = "zed_url did not match expected default URL" @@ -19,6 +33,20 @@ run "adds_folder" { folder = "/foo/bar" } + override_data { + target = data.coder_workspace.me + values = { + name = "default" + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + name = "default" + } + } + assert { condition = output.zed_url == "zed://ssh/default.coder/foo/bar" error_message = "zed_url did not include provided folder path" @@ -33,8 +61,54 @@ run "adds_agent_name" { agent_name = "myagent" } + override_data { + target = data.coder_workspace.me + values = { + name = "default" + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + name = "default" + } + } + assert { condition = output.zed_url == "zed://ssh/myagent.default.default.coder" error_message = "zed_url did not include agent_name in hostname" } } + +run "settings_base64_encoding" { + command = apply + + variables { + agent_id = "foo" + settings = jsonencode({ + theme = "dark" + fontSize = 14 + }) + } + + # Verify settings are base64 encoded (eyJ = base64 prefix for JSON starting with {") + assert { + condition = can(regex("SETTINGS_B64='eyJ", coder_script.zed_settings.script)) + error_message = "settings should be base64 encoded in the script" + } +} + +run "empty_settings" { + command = apply + + variables { + agent_id = "foo" + settings = "" + } + + assert { + condition = can(regex("SETTINGS_B64=''", coder_script.zed_settings.script)) + error_message = "empty settings should result in empty SETTINGS_B64" + } +} diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf index d391c75a..b7c6153d 100644 --- a/registry/coder/templates/kubernetes-devcontainer/main.tf +++ b/registry/coder/templates/kubernetes-devcontainer/main.tf @@ -139,7 +139,7 @@ variable "cache_repo_secret_name" { type = string } -data "kubernetes_secret" "cache_repo_dockerconfig_secret" { +data "kubernetes_secret_v1" "cache_repo_dockerconfig_secret" { count = var.cache_repo_secret_name == "" ? 0 : 1 metadata { name = var.cache_repo_secret_name @@ -166,7 +166,7 @@ locals { # Use the docker gateway if the access URL is 127.0.0.1 "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, - "ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")), + "ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret_v1.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")), "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true" # You may need to adjust this if you get an error regarding deleting files when building the workspace. # For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in @@ -186,7 +186,7 @@ resource "envbuilder_cached_image" "cached" { insecure = var.insecure_cache_repo } -resource "kubernetes_persistent_volume_claim" "workspaces" { +resource "kubernetes_persistent_volume_claim_v1" "workspaces" { metadata { name = "coder-${lower(data.coder_workspace.me.id)}-workspaces" namespace = var.namespace @@ -217,10 +217,10 @@ resource "kubernetes_persistent_volume_claim" "workspaces" { } } -resource "kubernetes_deployment" "main" { +resource "kubernetes_deployment_v1" "main" { count = data.coder_workspace.me.start_count depends_on = [ - kubernetes_persistent_volume_claim.workspaces + kubernetes_persistent_volume_claim_v1.workspaces ] wait_for_rollout = false metadata { @@ -300,7 +300,7 @@ resource "kubernetes_deployment" "main" { volume { name = "workspaces" persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name + claim_name = kubernetes_persistent_volume_claim_v1.workspaces.metadata.0.name read_only = false } } diff --git a/registry/coder/templates/kubernetes-envbox/main.tf b/registry/coder/templates/kubernetes-envbox/main.tf index 98543d9c..b7693cbf 100644 --- a/registry/coder/templates/kubernetes-envbox/main.tf +++ b/registry/coder/templates/kubernetes-envbox/main.tf @@ -106,22 +106,20 @@ module "code-server" { # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. version = "~> 1.0" - agent_id = coder_agent.main.id - agent_name = "main" - order = 1 + agent_id = coder_agent.main.id + order = 1 } # See https://registry.coder.com/modules/coder/jetbrains module "jetbrains" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/modules/coder/jetbrains/coder" - version = "~> 1.0" - agent_id = coder_agent.main.id - agent_name = "main" - folder = "/home/coder" + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "~> 1.0" + agent_id = coder_agent.main.id + folder = "/home/coder" } -resource "kubernetes_persistent_volume_claim" "home" { +resource "kubernetes_persistent_volume_claim_v1" "home" { metadata { name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home" namespace = var.namespace @@ -137,7 +135,7 @@ resource "kubernetes_persistent_volume_claim" "home" { } } -resource "kubernetes_pod" "main" { +resource "kubernetes_pod_v1" "main" { count = data.coder_workspace.me.start_count metadata { @@ -284,7 +282,7 @@ resource "kubernetes_pod" "main" { volume { name = "home" persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name + claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name read_only = false } } diff --git a/registry/coder/templates/kubernetes/main.tf b/registry/coder/templates/kubernetes/main.tf index 7d7c0aa8..c324331f 100644 --- a/registry/coder/templates/kubernetes/main.tf +++ b/registry/coder/templates/kubernetes/main.tf @@ -192,7 +192,7 @@ resource "coder_app" "code-server" { } } -resource "kubernetes_persistent_volume_claim" "home" { +resource "kubernetes_persistent_volume_claim_v1" "home" { metadata { name = "coder-${data.coder_workspace.me.id}-home" namespace = var.namespace @@ -222,10 +222,10 @@ resource "kubernetes_persistent_volume_claim" "home" { } } -resource "kubernetes_deployment" "main" { +resource "kubernetes_deployment_v1" "main" { count = data.coder_workspace.me.start_count depends_on = [ - kubernetes_persistent_volume_claim.home + kubernetes_persistent_volume_claim_v1.home ] wait_for_rollout = false metadata { @@ -316,7 +316,7 @@ resource "kubernetes_deployment" "main" { volume { name = "home" persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name + claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name read_only = false } } diff --git a/registry/cytoshahar/.images/avatar.jpeg b/registry/cytoshahar/.images/avatar.jpeg index 3e82d763..245aa94a 100644 Binary files a/registry/cytoshahar/.images/avatar.jpeg and b/registry/cytoshahar/.images/avatar.jpeg differ diff --git a/registry/djarbz/.images/avatar.png b/registry/djarbz/.images/avatar.png index f6019203..a5f427f9 100644 Binary files a/registry/djarbz/.images/avatar.png and b/registry/djarbz/.images/avatar.png differ diff --git a/registry/harleylrn/.images/avatar.png b/registry/harleylrn/.images/avatar.png index 08e6f54e..f99ed608 100644 Binary files a/registry/harleylrn/.images/avatar.png and b/registry/harleylrn/.images/avatar.png differ diff --git a/registry/harleylrn/.images/kiro-cli.png b/registry/harleylrn/.images/kiro-cli.png index d325299b..3f4d5cf6 100644 Binary files a/registry/harleylrn/.images/kiro-cli.png and b/registry/harleylrn/.images/kiro-cli.png differ 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..2131fbd0 --- /dev/null +++ b/registry/nboyers/templates/cloud-dev/README.md @@ -0,0 +1,73 @@ +--- +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: ../../../../.icons/cloud-devops.svg +verified: false +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/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 +} + + diff --git a/registry/umair/.images/avatar.jpeg b/registry/umair/.images/avatar.jpeg index 6495306b..5afcd706 100644 Binary files a/registry/umair/.images/avatar.jpeg and b/registry/umair/.images/avatar.jpeg differ