Merge branch 'main' into jwb/claude-code-pre-start-script

This commit is contained in:
DevCats 2026-01-05 16:36:51 -06:00 committed by GitHub
commit 6ad0552579
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 2703 additions and 307 deletions

View File

@ -1,14 +1,18 @@
#!/bin/bash #!/bin/bash
# Version Bump Script # Version Bump Script
# Usage: ./version-bump.sh <bump_type> [base_ref] # Usage: ./version-bump.sh [--ci] <bump_type> [base_ref]
# --ci: CI mode - run bump, check for changes, exit 1 if changes needed
# bump_type: patch, minor, or major # bump_type: patch, minor, or major
# base_ref: base reference for diff (default: origin/main) # base_ref: base reference for diff (default: origin/main)
set -euo pipefail set -euo pipefail
CI_MODE=false
usage() { usage() {
echo "Usage: $0 <bump_type> [base_ref]" echo "Usage: $0 [--ci] <bump_type> [base_ref]"
echo " --ci: CI mode - validates versions are already bumped (exits 1 if not)"
echo " bump_type: patch, minor, or major" echo " bump_type: patch, minor, or major"
echo " base_ref: base reference for diff (default: origin/main)" echo " base_ref: base reference for diff (default: origin/main)"
echo "" echo ""
@ -16,6 +20,7 @@ usage() {
echo " $0 patch # Update versions with patch bump" echo " $0 patch # Update versions with patch bump"
echo " $0 minor # Update versions with minor bump" echo " $0 minor # Update versions with minor bump"
echo " $0 major # Update versions with major bump" echo " $0 major # Update versions with major bump"
echo " $0 --ci patch # CI check: verify patch bump has been applied"
exit 1 exit 1
} }
@ -85,7 +90,7 @@ update_readme_version() {
in_module_block = 0 in_module_block = 0
if (module_has_target_source) { if (module_has_target_source) {
num_lines = split(module_content, lines, "\n") num_lines = split(module_content, lines, "\n")
for (i = 1; i <= num_lines; i++) { for (i = 1; i < num_lines; i++) {
line = lines[i] line = lines[i]
if (line ~ /^[[:space:]]*version[[:space:]]*=/) { if (line ~ /^[[:space:]]*version[[:space:]]*=/) {
match(line, /^[[:space:]]*/) match(line, /^[[:space:]]*/)
@ -115,6 +120,11 @@ update_readme_version() {
} }
main() { main() {
if [ "${1:-}" = "--ci" ]; then
CI_MODE=true
shift
fi
if [ $# -lt 1 ] || [ $# -gt 2 ]; then if [ $# -lt 1 ] || [ $# -gt 2 ]; then
usage usage
fi fi
@ -152,6 +162,8 @@ main() {
local untagged_modules="" local untagged_modules=""
local has_changes=false local has_changes=false
declare -a modified_readme_files=()
while IFS= read -r module_path; do while IFS= read -r module_path; do
if [ -z "$module_path" ]; then continue; fi if [ -z "$module_path" ]; then continue; fi
@ -202,6 +214,7 @@ main() {
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
updated_readmes="$updated_readmes\n- $namespace/$module_name" updated_readmes="$updated_readmes\n- $namespace/$module_name"
modified_readme_files+=("$readme_path")
has_changes=true has_changes=true
fi fi
@ -210,19 +223,22 @@ main() {
done <<< "$modules" done <<< "$modules"
# Always run formatter to ensure consistent formatting if [ ${#modified_readme_files[@]} -gt 0 ]; then
echo "🔧 Running formatter to ensure consistent formatting..." echo "🔧 Formatting modified README files..."
if command -v bun > /dev/null 2>&1; then if command -v bun > /dev/null 2>&1; then
bun fmt > /dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..." for readme_file in "${modified_readme_files[@]}"; do
bun run prettier --write "$readme_file" 2> /dev/null || true
done
else else
echo "⚠️ Warning: bun not found, skipping formatting" echo "⚠️ Warning: bun not found, skipping formatting"
fi fi
echo "" echo ""
fi
echo "📋 Summary:" echo "📋 Summary:"
echo "Bump Type: $bump_type" echo "Bump Type: $bump_type"
echo "" echo ""
echo "Modules Updated:" echo "Modules Processed:"
echo -e "$bumped_modules" echo -e "$bumped_modules"
echo "" echo ""
@ -239,6 +255,19 @@ main() {
echo "" echo ""
fi 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 if [ "$has_changes" = true ]; then
echo "✅ Version bump completed successfully!" echo "✅ Version bump completed successfully!"
echo "📝 README files have been updated with new versions." echo "📝 README files have been updated with new versions."

View File

@ -93,7 +93,7 @@ jobs:
- name: Validate formatting - name: Validate formatting
run: bun fmt:ci run: bun fmt:ci
- name: Check for typos - name: Check for typos
uses: crate-ci/typos@v1.40.0 uses: crate-ci/typos@v1.41.0
with: with:
config: .github/typos.toml config: .github/typos.toml
validate-readme-files: validate-readme-files:

View File

@ -55,62 +55,35 @@ jobs:
;; ;;
esac esac
- name: Check version bump requirements - name: Check version bump
id: version-check run: ./.github/scripts/version-bump.sh --ci "${{ steps.bump-type.outputs.type }}" origin/main
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: Comment on PR - Version bump required
echo "output<<EOF" if: failure()
cat "$output_file"
echo "EOF"
} >> $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'
uses: actions/github-script@v8 uses: actions/github-script@v8
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
const output = `${{ steps.version-check.outputs.output }}`;
const bumpType = `${{ steps.bump-type.outputs.type }}`; const bumpType = `${{ steps.bump-type.outputs.type }}`;
let comment = `## ❌ Version Bump Validation Failed\n\n`; const comment = [
comment += `**Bump Type:** \`${bumpType}\`\n\n`; '## Version Bump Required',
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`; '',
comment += `**Required Actions:**\n`; 'One or more modules in this PR need their versions updated.',
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`; '**To fix this:**',
comment += `3. Push the changes: \`git push\`\n\n`; '1. Run the version bump script locally:',
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`; ' ```bash',
comment += `> Please update the module versions and push the changes to continue.`; ` ./.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({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
# Logs # Logs
logs logs
*.log *.log

29
.icons/cloud-devops.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

2
.icons/scaleway.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>Scaleway icon</title><path d="M16.61 11.11v5.72a1.77 1.77 0 0 1-1.54 1.69h-4a1.43 1.43 0 0 1-1.31-1.22 1.09 1.09 0 0 1 0-.18 1.37 1.37 0 0 1 1.37-1.36h1.74a1 1 0 0 0 1-1v-3.62a1.4 1.4 0 0 1 1.18-1.39h.17a1.37 1.37 0 0 1 1.39 1.36zm-6.46 1.74V9.26a1 1 0 0 1 1-1H13a1.37 1.37 0 0 0 1.37-1.37 1 1 0 0 0 0-.17 1.45 1.45 0 0 0-1.41-1.2H9a1.81 1.81 0 0 0-1.58 1.66v5.7a1.37 1.37 0 0 0 1.37 1.37H9a1.4 1.4 0 0 0 1.15-1.4zm12-4.29V20A4.53 4.53 0 0 1 18 24h-7.58a8.57 8.57 0 0 1-8.56-8.57V4.54A4.54 4.54 0 0 1 6.4 0h7.18a8.56 8.56 0 0 1 8.56 8.56zm-2.74 0a5.83 5.83 0 0 0-5.82-5.82H6.4a1.79 1.79 0 0 0-1.8 1.8v10.89a5.83 5.83 0 0 0 5.82 5.8h7.44a1.79 1.79 0 0 0 1.54-1.48z"/></svg>

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
```tf ```tf
module "tmux" { module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder" source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.3" version = "1.0.4"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -39,7 +39,7 @@ module "tmux" {
```tf ```tf
module "tmux" { module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder" source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.3" version = "1.0.4"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes 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 ```tf
module "tmux" { module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder" source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.3" version = "1.0.4"
agent_id = var.agent_id agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"] sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT tmux_config = <<-EOT

View File

@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
display_name = "tmux" display_name = "tmux"
icon = "/icon/terminal.svg" icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", { script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = var.tmux_config TMUX_CONFIG = base64encode(var.tmux_config)
SAVE_INTERVAL = var.save_interval SAVE_INTERVAL = var.save_interval
}) })
run_on_start = true run_on_start = true

View File

@ -4,7 +4,7 @@ BOLD='\033[0;1m'
# Convert templated variables to shell variables # Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}" SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG="${TMUX_CONFIG}" TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
# Function to install tmux # Function to install tmux
install_tmux() { install_tmux() {
@ -73,7 +73,7 @@ setup_tmux_config() {
mkdir -p "$config_dir" mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then 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" printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else else
cat > "$config_file" << EOF cat > "$config_file" << EOF

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 KiB

After

Width:  |  Height:  |  Size: 590 KiB

View File

@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
```tf ```tf
module "auggie" { module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder" source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -47,7 +47,7 @@ module "coder-login" {
module "auggie" { module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder" source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
@ -103,7 +103,7 @@ EOF
```tf ```tf
module "auggie" { module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder" source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.2.2" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"

View File

@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.7" version = ">= 2.12"
} }
} }
} }
@ -179,7 +179,7 @@ locals {
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0" version = "2.0.0"
agent_id = var.agent_id agent_id = var.agent_id
folder = local.folder folder = local.folder
@ -230,3 +230,7 @@ module "agentapi" {
/tmp/install.sh /tmp/install.sh
EOT EOT
} }
output "task_app_id" {
value = module.agentapi.task_app_id
}

View File

@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
```tf ```tf
module "copilot" { module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder" source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder/projects" workdir = "/home/coder/projects"
} }
@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" {
module "copilot" { module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder" source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder/projects" workdir = "/home/coder/projects"
@ -71,7 +71,7 @@ Customize tool permissions, MCP servers, and Copilot settings:
```tf ```tf
module "copilot" { module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder" source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder/projects" workdir = "/home/coder/projects"
@ -142,7 +142,7 @@ variable "github_token" {
module "copilot" { module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder" source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder/projects" workdir = "/home/coder/projects"
github_token = var.github_token github_token = var.github_token
@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
```tf ```tf
module "copilot" { module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder" source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.3" version = "0.3.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
workdir = "/home/coder" workdir = "/home/coder"
report_tasks = false report_tasks = false

View File

@ -3,7 +3,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.7" version = ">= 2.12"
} }
} }
} }
@ -242,7 +242,7 @@ resource "coder_env" "github_token" {
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0" version = "2.0.0"
agent_id = var.agent_id agent_id = var.agent_id
folder = local.workdir folder = local.workdir
@ -300,3 +300,7 @@ module "agentapi" {
/tmp/install.sh /tmp/install.sh
EOT EOT
} }
output "task_app_id" {
value = module.agentapi.task_app_id
}

View File

@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
```tf ```tf
module "cursor_cli" { module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder" source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.2" version = "0.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -42,7 +42,7 @@ module "coder-login" {
module "cursor_cli" { module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder" source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.2.2" version = "0.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"

View File

@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
"-c", "-c",
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true", "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("-f");
expect(startLog.stdout).toContain("test prompt"); expect(startLog.stdout).toContain("test prompt");
}); });

View File

@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.7" version = ">= 2.12"
} }
} }
} }
@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0" version = "2.0.0"
agent_id = var.agent_id agent_id = var.agent_id
folder = local.folder folder = local.folder
@ -179,3 +179,7 @@ module "agentapi" {
/tmp/install.sh /tmp/install.sh
EOT EOT
} }
output "task_app_id" {
value = module.agentapi.task_app_id
}

View File

@ -50,7 +50,7 @@ ARGS=()
# global flags # global flags
if [ -n "$ARG_MODEL" ]; then if [ -n "$ARG_MODEL" ]; then
ARGS+=("-m" "$ARG_MODEL") ARGS+=("--model" "$ARG_MODEL")
fi fi
if [ "$ARG_FORCE" = "true" ]; then if [ "$ARG_FORCE" = "true" ]; then
ARGS+=("-f") ARGS+=("-f")

View File

@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf ```tf
module "gemini" { module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder" source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2" version = "3.0.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" { module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder" source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2" version = "3.0.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key gemini_api_key = var.gemini_api_key
folder = "/home/coder/project" folder = "/home/coder/project"
@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" { module "gemini" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder" source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2" version = "3.0.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash" gemini_model = "gemini-2.5-flash"
@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf ```tf
module "gemini" { module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder" source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.1.2" version = "3.0.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
gemini_api_key = var.gemini_api_key gemini_api_key = var.gemini_api_key
folder = "/home/coder/project" folder = "/home/coder/project"

View File

@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.7" version = ">= 2.12"
} }
} }
} }
@ -177,7 +177,7 @@ EOT
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0" version = "2.0.0"
agent_id = var.agent_id agent_id = var.agent_id
folder = local.folder folder = local.folder
@ -226,3 +226,7 @@ module "agentapi" {
/tmp/start.sh /tmp/start.sh
EOT EOT
} }
output "task_app_id" {
value = module.agentapi.task_app_id
}

View File

@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf ```tf
module "amp-cli" { module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "2.1.0" version = "3.0.0"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key amp_api_key = var.amp_api_key
install_amp = true install_amp = true
@ -48,7 +48,7 @@ variable "amp_api_key" {
module "amp-cli" { module "amp-cli" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" 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 agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project" workdir = "/home/coder/project"

View File

@ -110,6 +110,7 @@ describe("amp", async () => {
const { id } = await setup({ const { id } = await setup({
skipAmpMock: true, skipAmpMock: true,
moduleVariables: { moduleVariables: {
install_via_npm: "true",
amp_version: "0.0.1755964909-g31e083", amp_version: "0.0.1755964909-g31e083",
}, },
}); });

View File

@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.7" version = ">= 2.12"
} }
external = { external = {
source = "hashicorp/external" source = "hashicorp/external"
@ -220,7 +220,7 @@ locals {
module "agentapi" { module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder" source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0" version = "2.0.0"
agent_id = var.agent_id agent_id = var.agent_id
folder = local.workdir folder = local.workdir
@ -268,4 +268,6 @@ module "agentapi" {
EOT EOT
} }
output "task_app_id" {
value = module.agentapi.task_app_id
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View File

@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf ```tf
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx" 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 ```tf
module "claude-code" { module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder" source = "dev.registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
enable_boundary = true enable_boundary = true
@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
@ -108,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf ```tf
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
install_claude_code = true install_claude_code = true
@ -130,7 +130,7 @@ variable "claude_code_oauth_token" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token claude_code_oauth_token = var.claude_code_oauth_token
@ -203,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@ -260,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" { module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder" source = "registry.coder.com/coder/claude-code/coder"
version = "4.2.7" version = "4.2.8"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
workdir = "/home/coder/project" workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514" model = "claude-sonnet-4@20250514"

View File

@ -86,7 +86,7 @@ variable "install_agentapi" {
variable "agentapi_version" { variable "agentapi_version" {
type = string type = string
description = "The version of AgentAPI to install." description = "The version of AgentAPI to install."
default = "v0.11.4" default = "v0.11.6"
} }
variable "ai_prompt" { variable "ai_prompt" {

View File

@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -29,9 +29,9 @@ module "code-server" {
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ extensions = [
"dracula-theme.theme-dracula" "dracula-theme.theme-dracula"
@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"] extensions = ["dracula-theme.theme-dracula"]
settings = { settings = {
@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
additional_args = "--disable-workspace-trust" 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
use_cached = true use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] 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" { module "code-server" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder" source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1" version = "1.4.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
offline = true 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).

View File

@ -14,7 +14,7 @@ A file browser for your workspace.
module "filebrowser" { module "filebrowser" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder" source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -29,7 +29,7 @@ module "filebrowser" {
module "filebrowser" { module "filebrowser" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder" source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -41,7 +41,7 @@ module "filebrowser" {
module "filebrowser" { module "filebrowser" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder" source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
database_path = ".config/filebrowser.db" database_path = ".config/filebrowser.db"
} }
@ -53,7 +53,7 @@ module "filebrowser" {
module "filebrowser" { module "filebrowser" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder" source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
agent_name = "main" agent_name = "main"
subdomain = false subdomain = false

View File

@ -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} filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
fi 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" printf "👷 Starting filebrowser in background... \n\n"

View File

@ -1,9 +1,17 @@
import { type Server, serve } from "bun"; import { serve } from "bun";
import { describe, expect, it } from "bun:test"; import {
afterEach,
beforeAll,
describe,
expect,
it,
setDefaultTimeout,
} from "bun:test";
import { import {
createJSONResponse, createJSONResponse,
execContainer, execContainer,
findResourceInstance, findResourceInstance,
removeContainer,
runContainer, runContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
@ -11,77 +19,48 @@ import {
writeCoder, writeCoder,
} from "~test"; } from "~test";
describe("github-upload-public-key", async () => { let cleanupFunctions: (() => Promise<void>)[] = [];
await runTerraformInit(import.meta.dir); const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
testRequiredVariables(import.meta.dir, { };
agent_id: "foo", afterEach(async () => {
}); const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
it("creates new key if one does not exist", async () => { for (const cleanup of cleanupFnsCopy) {
const { instance, id, server } = await setupContainer(); try {
await writeCoder(id, "echo foo"); await cleanup();
} catch (error) {
const url = server.url.toString().slice(0, -1); console.error("Error during cleanup:", error);
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);
});
}); });
const setupContainer = async ( const setupContainer = async (
image = "lorello/alpine-bash", image = "lorello/alpine-bash",
vars: Record<string, string> = {}, vars: Record<string, string> = {},
) => { ) => {
const server = await setupServer(); const server = setupServer();
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
...vars, ...vars,
}); });
const instance = findResourceInstance(state, "coder_script"); const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image); const id = await runContainer(image);
registerCleanup(async () => {
server.stop();
});
registerCleanup(async () => {
await removeContainer(id);
});
return { id, instance, server }; return { id, instance, server };
}; };
const setupServer = async (): Promise<Server> => { const setupServer = () => {
let url: URL; const fakeGithubHost = serve({
const fakeSlackHost = serve({
fetch: (req) => { fetch: (req) => {
url = new URL(req.url); const url = new URL(req.url);
if (url.pathname === "/api/v2/users/me/gitsshkey") { if (url.pathname === "/api/v2/users/me/gitsshkey") {
return createJSONResponse({ return createJSONResponse({
public_key: "exists", public_key: "exists",
@ -128,5 +107,60 @@ const setupServer = async (): Promise<Server> => {
port: 0, 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);
});
});

View File

@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -37,7 +37,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -48,7 +48,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin # Default is "latest"; set to a specific version to pin
install_version = "0.4.0" install_version = "0.4.0"
@ -61,7 +61,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
port = 8080 port = 8080
} }
@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
use_cached = true use_cached = true
} }
@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.0.5" version = "1.0.6"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
install = false install = false
} }

View File

@ -97,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
fi fi
# sed-based fallback # sed-based fallback
if [ -z "$TARBALL_URL" ]; then 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 fi
# Fallback: resolve version then construct tarball URL # Fallback: resolve version then construct tarball URL
if [ -z "$TARBALL_URL" ]; then 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){}')" 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 fi
if [ -z "$RESOLVED_VERSION" ]; then 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 fi
if [ -z "$RESOLVED_VERSION" ]; then 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 fi
if [ -n "$RESOLVED_VERSION" ]; then if [ -n "$RESOLVED_VERSION" ]; then
VERSION_TO_USE="$RESOLVED_VERSION" VERSION_TO_USE="$RESOLVED_VERSION"
@ -141,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
fi fi
if [ -z "$BIN_PATH" ]; then if [ -z "$BIN_PATH" ]; then
# sed fallbacks (handle both string and object forms) # 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 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
fi fi
if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then

View File

@ -13,7 +13,7 @@ Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" 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 ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
vault_token = var.vault_token # Optional vault_token = var.vault_token # Optional
@ -50,7 +50,7 @@ Install the Vault CLI without any authentication:
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
} }
@ -61,7 +61,7 @@ module "vault_cli" {
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0" vault_cli_version = "1.15.0"
@ -73,7 +73,7 @@ module "vault_cli" {
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
install_dir = "/home/coder/bin" install_dir = "/home/coder/bin"
@ -87,7 +87,7 @@ For Vault Enterprise users who need to specify a namespace:
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
vault_token = var.vault_token vault_token = var.vault_token
@ -102,7 +102,7 @@ Install the Vault Enterprise binary. This is required if using SAML authenticati
```tf ```tf
module "vault_cli" { module "vault_cli" {
source = "registry.coder.com/coder/vault-cli/coder" source = "registry.coder.com/coder/vault-cli/coder"
version = "1.1.0" version = "1.1.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
enterprise = true enterprise = true

View File

@ -7,40 +7,34 @@ INSTALL_DIR=${INSTALL_DIR}
VAULT_CLI_VERSION=${VAULT_CLI_VERSION} VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
ENTERPRISE=${ENTERPRISE} ENTERPRISE=${ENTERPRISE}
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout. # Fetch URL content to stdout
# Usage: fetch <url> [dest]
fetch() { fetch() {
url="$1" url="$1"
dest="$${2:-}"
# Detect HTTP client on first run
if [ -z "$${HTTP_CLIENT:-}" ]; then
if command -v curl > /dev/null 2>&1; then if command -v curl > /dev/null 2>&1; then
HTTP_CLIENT="curl" curl -sSL --fail "$${url}"
elif command -v wget > /dev/null 2>&1; then elif command -v wget > /dev/null 2>&1; then
HTTP_CLIENT="wget" wget -qO- "$${url}"
elif command -v busybox > /dev/null 2>&1; then elif command -v busybox > /dev/null 2>&1; then
HTTP_CLIENT="busybox" busybox wget -qO- "$${url}"
else else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n" printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1 return 1
fi fi
fi }
if [ -n "$${dest}" ]; then # Download URL to a file
# shellcheck disable=SC2195 fetch_to_file() {
case "$${HTTP_CLIENT}" in dest="$1"
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;; url="$2"
wget) wget -O "$${dest}" "$${url}" ;; if command -v curl > /dev/null 2>&1; then
busybox) busybox wget -O "$${dest}" "$${url}" ;; curl -sSL --fail "$${url}" -o "$${dest}"
esac 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 else
# shellcheck disable=SC2195 printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
case "$${HTTP_CLIENT}" in return 1
curl) curl -sSL --fail "$${url}" ;;
wget) wget -qO- "$${url}" ;;
busybox) busybox wget -qO- "$${url}" ;;
esac
fi fi
} }
@ -141,7 +135,7 @@ install() {
cd "$${TEMP_DIR}" || return 1 cd "$${TEMP_DIR}" || return 1
printf "Downloading from %s\n" "$${DOWNLOAD_URL}" 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" printf "Failed to download Vault.\n"
rm -rf "$${TEMP_DIR}" rm -rf "$${TEMP_DIR}"
return 1 return 1

View File

@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and
module "zed" { module "zed" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/zed/coder" source = "registry.coder.com/coder/zed/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -32,7 +32,7 @@ module "zed" {
module "zed" { module "zed" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/zed/coder" source = "registry.coder.com/coder/zed/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@ -44,7 +44,7 @@ module "zed" {
module "zed" { module "zed" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/zed/coder" source = "registry.coder.com/coder/zed/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
display_name = "Zed Editor" display_name = "Zed Editor"
order = 1 order = 1
@ -57,7 +57,7 @@ module "zed" {
module "zed" { module "zed" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/zed/coder" source = "registry.coder.com/coder/zed/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
agent_name = coder_agent.example.name 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" { module "zed" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/zed/coder" source = "registry.coder.com/coder/zed/coder"
version = "1.1.3" version = "1.1.4"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
settings = jsonencode({ settings = jsonencode({
@ -85,6 +85,7 @@ module "zed" {
env = {} env = {}
} }
} }
}) })
} }

View File

@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@ -12,66 +16,114 @@ describe("zed", async () => {
agent_id: "foo", 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, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", 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",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
}); });
it("adds folder", async () => { const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine:latest");
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,
};
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", 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, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
order: "22", settings: "",
}); });
const coder_app = state.resources.find( const instance = findResourceInstance(state, "coder_script");
(res) => res.type === "coder_app" && res.name === "zed", const id = await runContainer("alpine:latest");
);
expect(coder_app).not.toBeNull(); try {
expect(coder_app?.instances.length).toBe(1); const result = await execContainer(id, ["sh", "-c", instance.script]);
expect(coder_app?.instances[0].attributes.order).toBe(22); expect(result.exitCode).toBe(0);
});
it("expect display_name to be set", async () => { // Settings file should not be created
const state = await runTerraformApply(import.meta.dir, { const catResult = await execContainer(id, [
agent_id: "foo", "cat",
display_name: "Custom Zed", "/root/.config/zed/settings.json",
}); ]);
expect(catResult.exitCode).not.toBe(0);
const coder_app = state.resources.find( } finally {
(res) => res.type === "coder_app" && res.name === "zed", await removeContainer(id);
); }
}, 30000);
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",
);
});
}); });

View File

@ -65,6 +65,7 @@ locals {
owner_name = lower(data.coder_workspace_owner.me.name) owner_name = lower(data.coder_workspace_owner.me.name)
agent_name = lower(var.agent_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" 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" { resource "coder_script" "zed_settings" {
@ -75,7 +76,11 @@ resource "coder_script" "zed_settings" {
script = <<-EOT script = <<-EOT
#!/usr/bin/env bash #!/usr/bin/env bash
set -eu 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 if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
exit 0 exit 0
fi fi

View File

@ -5,6 +5,20 @@ run "default_output" {
agent_id = "foo" 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 { assert {
condition = output.zed_url == "zed://ssh/default.coder" condition = output.zed_url == "zed://ssh/default.coder"
error_message = "zed_url did not match expected default URL" error_message = "zed_url did not match expected default URL"
@ -19,6 +33,20 @@ run "adds_folder" {
folder = "/foo/bar" 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 { assert {
condition = output.zed_url == "zed://ssh/default.coder/foo/bar" condition = output.zed_url == "zed://ssh/default.coder/foo/bar"
error_message = "zed_url did not include provided folder path" error_message = "zed_url did not include provided folder path"
@ -33,8 +61,54 @@ run "adds_agent_name" {
agent_name = "myagent" 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 { assert {
condition = output.zed_url == "zed://ssh/myagent.default.default.coder" condition = output.zed_url == "zed://ssh/myagent.default.default.coder"
error_message = "zed_url did not include agent_name in hostname" 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"
}
}

View File

@ -139,7 +139,7 @@ variable "cache_repo_secret_name" {
type = string 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 count = var.cache_repo_secret_name == "" ? 0 : 1
metadata { metadata {
name = var.cache_repo_secret_name name = var.cache_repo_secret_name
@ -166,7 +166,7 @@ locals {
# Use the docker gateway if the access URL is 127.0.0.1 # 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_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_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" "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. # 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 # 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 insecure = var.insecure_cache_repo
} }
resource "kubernetes_persistent_volume_claim" "workspaces" { resource "kubernetes_persistent_volume_claim_v1" "workspaces" {
metadata { metadata {
name = "coder-${lower(data.coder_workspace.me.id)}-workspaces" name = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
namespace = var.namespace 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 count = data.coder_workspace.me.start_count
depends_on = [ depends_on = [
kubernetes_persistent_volume_claim.workspaces kubernetes_persistent_volume_claim_v1.workspaces
] ]
wait_for_rollout = false wait_for_rollout = false
metadata { metadata {
@ -300,7 +300,7 @@ resource "kubernetes_deployment" "main" {
volume { volume {
name = "workspaces" name = "workspaces"
persistent_volume_claim { 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 read_only = false
} }
} }

View File

@ -107,21 +107,19 @@ module "code-server" {
version = "~> 1.0" version = "~> 1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
agent_name = "main"
order = 1 order = 1
} }
# See https://registry.coder.com/modules/coder/jetbrains # See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" { module "jetbrains" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder" source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.0" version = "~> 1.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder" folder = "/home/coder"
} }
resource "kubernetes_persistent_volume_claim" "home" { resource "kubernetes_persistent_volume_claim_v1" "home" {
metadata { metadata {
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home" name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
namespace = var.namespace 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 count = data.coder_workspace.me.start_count
metadata { metadata {
@ -284,7 +282,7 @@ resource "kubernetes_pod" "main" {
volume { volume {
name = "home" name = "home"
persistent_volume_claim { 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 read_only = false
} }
} }

View File

@ -192,7 +192,7 @@ resource "coder_app" "code-server" {
} }
} }
resource "kubernetes_persistent_volume_claim" "home" { resource "kubernetes_persistent_volume_claim_v1" "home" {
metadata { metadata {
name = "coder-${data.coder_workspace.me.id}-home" name = "coder-${data.coder_workspace.me.id}-home"
namespace = var.namespace 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 count = data.coder_workspace.me.start_count
depends_on = [ depends_on = [
kubernetes_persistent_volume_claim.home kubernetes_persistent_volume_claim_v1.home
] ]
wait_for_rollout = false wait_for_rollout = false
metadata { metadata {
@ -316,7 +316,7 @@ resource "kubernetes_deployment" "main" {
volume { volume {
name = "home" name = "home"
persistent_volume_claim { 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 read_only = false
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,2 @@
#!/bin/bash
sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}'

View File

@ -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"
}

View File

@ -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"
}
]

41
registry/nboyers/.gitignore vendored Normal file
View File

@ -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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -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.

View File

@ -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": "*"
}
```

View File

@ -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]
}

View File

@ -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."

View File

@ -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:-<none>} @ ${DEFAULT_BRANCH}"
echo " • Auth helpers: source ~/workspace/cloud-auth.sh"

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 631 KiB