mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dbdad101d | |||
| 2ecdbddf8f | |||
| fa8147e1bb | |||
| 4aa4448d81 | |||
| 3829a20756 | |||
| 15a16f279b | |||
| a6aca54728 | |||
| b2c1ff0770 | |||
| 41e86b8715 | |||
| 4f22907ed4 | |||
| 65ccc6ff48 | |||
| 2701dc09af | |||
| 60611ed593 | |||
| 7df0cb25c5 | |||
| cbb39bda6f | |||
| 99bd4a4139 | |||
| c819ca7f83 | |||
| accf5a34ab | |||
| bb222f36a5 | |||
| 3677e93e36 | |||
| a3ba616aec | |||
| 24dc52fb17 | |||
| 678c3e631e | |||
| 36089612ef | |||
| ac44ad862a |
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
uses: crate-ci/typos@v1.41.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
@@ -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 |
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"type_meta": {
|
||||
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
|
||||
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
|
||||
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
|
||||
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
|
||||
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
|
||||
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
|
||||
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
|
||||
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
|
||||
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
|
||||
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
|
||||
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
|
||||
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
|
||||
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
|
||||
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
|
||||
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
|
||||
},
|
||||
"availability": {
|
||||
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
|
||||
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
|
||||
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ terraform {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +21,24 @@ provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
data "http" "hcloud_locations" {
|
||||
url = "https://api.hetzner.cloud/v1/locations"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
data "http" "hcloud_server_types" {
|
||||
url = "https://api.hetzner.cloud/v1/server_types"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
@@ -24,29 +46,18 @@ data "coder_parameter" "hcloud_location" {
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
option {
|
||||
name = "DE Falkenstein"
|
||||
value = "fsn1"
|
||||
}
|
||||
option {
|
||||
name = "US Ashburn, VA"
|
||||
value = "ash"
|
||||
}
|
||||
option {
|
||||
name = "US Hillsboro, OR"
|
||||
value = "hil"
|
||||
}
|
||||
option {
|
||||
name = "SG Singapore"
|
||||
value = "sin"
|
||||
}
|
||||
option {
|
||||
name = "DE Nuremberg"
|
||||
value = "nbg1"
|
||||
}
|
||||
option {
|
||||
name = "FI Helsinki"
|
||||
value = "hel1"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_locations
|
||||
content {
|
||||
name = format(
|
||||
"%s (%s, %s)",
|
||||
upper(option.value.name),
|
||||
option.value.city,
|
||||
option.value.country
|
||||
)
|
||||
value = option.value.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +120,47 @@ resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# Data source: local JSON file under the module directory
|
||||
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
|
||||
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
|
||||
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
|
||||
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
|
||||
# --------------------
|
||||
# Locations
|
||||
# --------------------
|
||||
hcloud_locations = [
|
||||
for loc in jsondecode(data.http.hcloud_locations.response_body).locations : {
|
||||
name = loc.name
|
||||
city = loc.city
|
||||
country = loc.country
|
||||
}
|
||||
]
|
||||
|
||||
# --------------------
|
||||
# Server Types
|
||||
# --------------------
|
||||
hcloud_server_types = {
|
||||
for st in jsondecode(data.http.hcloud_server_types.response_body).server_types :
|
||||
st.name => {
|
||||
cores = st.cores
|
||||
memory_gb = st.memory
|
||||
disk_gb = st.disk
|
||||
locations = [for l in st.locations : l.name]
|
||||
deprecated = st.deprecated
|
||||
}
|
||||
if st.deprecated == false
|
||||
}
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
|
||||
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
|
||||
value = type_name
|
||||
for name, meta in local.hcloud_server_types : {
|
||||
name = format(
|
||||
"%s (%d vCPU, %dGB RAM, %dGB)",
|
||||
upper(name),
|
||||
meta.cores,
|
||||
meta.memory_gb,
|
||||
meta.disk_gb
|
||||
)
|
||||
value = name
|
||||
}
|
||||
if contains(
|
||||
meta.locations,
|
||||
data.coder_parameter.hcloud_location.value
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -180,4 +221,4 @@ module "code-server" {
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -39,7 +39,7 @@ module "tmux" {
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
tmux_config = "" # Optional: custom tmux.conf content
|
||||
save_interval = 1 # Optional: save interval in minutes
|
||||
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = var.agent_id
|
||||
sessions = ["default", "dev", "anomaly"]
|
||||
tmux_config = <<-EOT
|
||||
|
||||
@@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
|
||||
display_name = "tmux"
|
||||
icon = "/icon/terminal.svg"
|
||||
script = templatefile("${path.module}/scripts/run.sh", {
|
||||
TMUX_CONFIG = var.tmux_config
|
||||
TMUX_CONFIG = base64encode(var.tmux_config)
|
||||
SAVE_INTERVAL = var.save_interval
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -4,7 +4,7 @@ BOLD='\033[0;1m'
|
||||
|
||||
# Convert templated variables to shell variables
|
||||
SAVE_INTERVAL="${SAVE_INTERVAL}"
|
||||
TMUX_CONFIG="${TMUX_CONFIG}"
|
||||
TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
|
||||
|
||||
# Function to install tmux
|
||||
install_tmux() {
|
||||
@@ -73,7 +73,7 @@ setup_tmux_config() {
|
||||
mkdir -p "$config_dir"
|
||||
|
||||
if [ -n "$TMUX_CONFIG" ]; then
|
||||
printf "$TMUX_CONFIG" > "$config_file"
|
||||
printf "%s" "$TMUX_CONFIG" > "$config_file"
|
||||
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
|
||||
else
|
||||
cat > "$config_file" << EOF
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
#set -o errexit
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
port=${1:-3284}
|
||||
|
||||
@@ -10,18 +11,30 @@ port=${1:-3284}
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
start=$(date +%s)
|
||||
while true; do
|
||||
if curl -f "http://localhost:$port/status"; then
|
||||
agentapi_started=true
|
||||
elapsed=$(($(date +%s) - start))
|
||||
echo "$(date): agentapi server started after $elapsed seconds"
|
||||
break
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
echo "$(date): agentapi server not responding"
|
||||
agentapi_pid=$(pidof agentapi)
|
||||
if [ -z "$agentapi_pid" ]; then
|
||||
echo "$(date): agentapi process not found"
|
||||
else
|
||||
echo "$(date): agentapi pid: $agentapi_pid"
|
||||
fi
|
||||
boundary_pid=$(pidof boundary)
|
||||
if [ -z "$boundary_pid" ]; then
|
||||
echo "$(date): boundary process not found"
|
||||
else
|
||||
echo "$(date): boundary pid: $boundary_pid"
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
use_prompt=${1:-false}
|
||||
port=${2:-3284}
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
@@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -108,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
@@ -130,7 +130,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -203,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -260,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.7"
|
||||
version = "4.2.8"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -86,7 +86,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.11.4"
|
||||
default = "v0.11.6"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
@@ -338,8 +338,8 @@ locals {
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
source = "github.com/coder/registry//registry/coder/modules/agentapi?ref=cj%2Fagentapi%2Fdebug"
|
||||
# version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
|
||||
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -41,7 +41,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -53,7 +53,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then
|
||||
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
fi
|
||||
|
||||
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser config set --baseURL=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
|
||||
printf "👷 Starting filebrowser in background... \n\n"
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { type Server, serve } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { serve } from "bun";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
setDefaultTimeout,
|
||||
} from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
@@ -11,77 +19,48 @@ import {
|
||||
writeCoder,
|
||||
} from "~test";
|
||||
|
||||
describe("github-upload-public-key", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
// we need to increase timeout to pull the container
|
||||
}, 15000);
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const server = setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
registerCleanup(async () => {
|
||||
server.stop();
|
||||
});
|
||||
registerCleanup(async () => {
|
||||
await removeContainer(id);
|
||||
});
|
||||
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
const setupServer = () => {
|
||||
const fakeGithubHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
@@ -128,5 +107,60 @@ const setupServer = async (): Promise<Server> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,9 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
@@ -53,7 +52,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
@@ -67,7 +66,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
@@ -82,7 +81,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -109,7 +108,7 @@ module "jetbrains" {
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -129,11 +128,11 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -170,13 +169,6 @@ resource "coder_metadata" "container_info" {
|
||||
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
|
||||
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
|
||||
|
||||
### Tooltip
|
||||
|
||||
- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
|
||||
- If not specified, no tooltip is shown
|
||||
- Supports markdown formatting for rich text (bold, italic, links, etc.)
|
||||
- All IDE apps created by this module will show the same tooltip text
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
@@ -2,15 +2,15 @@ variables {
|
||||
# Default IDE config, mirrored from main.tf for test assertions.
|
||||
# If main.tf defaults change, update this map to match.
|
||||
expected_ide_config = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be set when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "tooltip_null_when_not_provided" {
|
||||
run "tooltip_default_when_not_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
@@ -206,8 +206,41 @@ run "tooltip_null_when_not_provided" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
|
||||
error_message = "Expected coder_app tooltip to be null when not provided"
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "channel_eap" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
channel = "eap"
|
||||
major_version = "latest"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.type == "eap"
|
||||
error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'"
|
||||
}
|
||||
}
|
||||
|
||||
run "specific_major_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
major_version = "2025.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3"
|
||||
error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,3 +327,27 @@ run "output_multiple_ides" {
|
||||
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
|
||||
}
|
||||
}
|
||||
run "validate_output_schema" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
for key, meta in output.ide_metadata : (
|
||||
can(meta.icon) &&
|
||||
can(meta.name) &&
|
||||
can(meta.identifier) &&
|
||||
can(meta.key) &&
|
||||
can(meta.build) &&
|
||||
# json_data can be null, but the key must exist
|
||||
can(meta.json_data)
|
||||
)
|
||||
])
|
||||
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
|
||||
variable "tooltip" {
|
||||
type = string
|
||||
description = "Markdown text that is displayed when hovering over workspace apps."
|
||||
default = null
|
||||
default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
variable "major_version" {
|
||||
@@ -70,8 +70,8 @@ variable "major_version" {
|
||||
description = "The major version of the IDE. i.e. 2025.1"
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
|
||||
condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ variable "download_base_link" {
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = length(var.default) == 0 ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
|
||||
}
|
||||
|
||||
variable "ide_config" {
|
||||
@@ -138,9 +138,9 @@ variable "ide_config" {
|
||||
- build: The build number of the IDE.
|
||||
Example:
|
||||
{
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
}
|
||||
EOT
|
||||
type = map(object({
|
||||
@@ -149,15 +149,15 @@ variable "ide_config" {
|
||||
build = string
|
||||
}))
|
||||
default = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
validation {
|
||||
condition = length(var.ide_config) > 0
|
||||
@@ -182,6 +182,20 @@ locals {
|
||||
)
|
||||
}
|
||||
|
||||
# Filter the parsed response for the requested major version if not "latest"
|
||||
filtered_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => [
|
||||
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) :
|
||||
r if var.major_version == "latest" || r.majorVersion == var.major_version
|
||||
]
|
||||
}
|
||||
|
||||
# Select the latest release for the requested major version (first item in the filtered list)
|
||||
selected_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code =>
|
||||
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
|
||||
}
|
||||
|
||||
# Dynamically generate IDE configurations based on options with fallback to ide_config
|
||||
options_metadata = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => {
|
||||
@@ -191,13 +205,10 @@ locals {
|
||||
key = code
|
||||
|
||||
# Use API build number if available, otherwise fall back to ide_config build number
|
||||
build = length(keys(local.parsed_responses[code])) > 0 ? (
|
||||
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
|
||||
) : var.ide_config[code].build
|
||||
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
|
||||
|
||||
# Store API data for potential future use (only if API is available)
|
||||
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
|
||||
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
|
||||
# Store API data for potential future use
|
||||
json_data = local.selected_releases[code]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -37,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -48,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -61,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
# sed-based fallback
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
# Fallback: resolve version then construct tarball URL
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
@@ -106,10 +106,10 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '"version":"[^"]*"' | head -n1 | cut -d '"' -f4)"
|
||||
fi
|
||||
if [ -n "$RESOLVED_VERSION" ]; then
|
||||
VERSION_TO_USE="$RESOLVED_VERSION"
|
||||
@@ -141,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
# sed fallbacks (handle both string and object forms)
|
||||
BIN_PATH=$(sed -n 's/.*\"bin\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
BIN_PATH=$(sed -n '/\"bin\"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)
|
||||
BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"mux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
fi
|
||||
fi
|
||||
if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then
|
||||
|
||||
@@ -13,7 +13,7 @@ Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ If you have a Vault token, you can provide it to automatically configure authent
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
@@ -50,7 +50,7 @@ Install the Vault CLI without any authentication:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
@@ -73,7 +73,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
@@ -87,7 +87,7 @@ For Vault Enterprise users who need to specify a namespace:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
@@ -102,7 +102,7 @@ Install the Vault Enterprise binary. This is required if using SAML authenticati
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
enterprise = true
|
||||
|
||||
@@ -7,40 +7,34 @@ INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
ENTERPRISE=${ENTERPRISE}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
# Fetch URL content to stdout
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -qO- "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -qO- "$${url}"
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Download URL to a file
|
||||
fetch_to_file() {
|
||||
dest="$1"
|
||||
url="$2"
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -O "$${dest}" "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -O "$${dest}" "$${url}"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -141,7 +135,7 @@ install() {
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
|
||||
@@ -139,7 +139,7 @@ variable "cache_repo_secret_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
data "kubernetes_secret" "cache_repo_dockerconfig_secret" {
|
||||
data "kubernetes_secret_v1" "cache_repo_dockerconfig_secret" {
|
||||
count = var.cache_repo_secret_name == "" ? 0 : 1
|
||||
metadata {
|
||||
name = var.cache_repo_secret_name
|
||||
@@ -166,7 +166,7 @@ locals {
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
|
||||
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
|
||||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")),
|
||||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret_v1.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")),
|
||||
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true"
|
||||
# You may need to adjust this if you get an error regarding deleting files when building the workspace.
|
||||
# For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in
|
||||
@@ -186,7 +186,7 @@ resource "envbuilder_cached_image" "cached" {
|
||||
insecure = var.insecure_cache_repo
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "workspaces" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "workspaces" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
|
||||
namespace = var.namespace
|
||||
@@ -217,10 +217,10 @@ resource "kubernetes_persistent_volume_claim" "workspaces" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
resource "kubernetes_deployment_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.workspaces
|
||||
kubernetes_persistent_volume_claim_v1.workspaces
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
@@ -300,7 +300,7 @@ resource "kubernetes_deployment" "main" {
|
||||
volume {
|
||||
name = "workspaces"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.workspaces.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,22 +106,20 @@ module "code-server" {
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 1
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/jetbrains
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/home/coder"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder"
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "home" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
@@ -137,7 +135,7 @@ resource "kubernetes_persistent_volume_claim" "home" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_pod" "main" {
|
||||
resource "kubernetes_pod_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
metadata {
|
||||
@@ -284,7 +282,7 @@ resource "kubernetes_pod" "main" {
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ resource "coder_app" "code-server" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "home" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
namespace = var.namespace
|
||||
@@ -222,10 +222,10 @@ resource "kubernetes_persistent_volume_claim" "home" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
resource "kubernetes_deployment_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.home
|
||||
kubernetes_persistent_volume_claim_v1.home
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
@@ -316,7 +316,7 @@ resource "kubernetes_deployment" "main" {
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}'
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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 |
@@ -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.
|
||||
@@ -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": "*"
|
||||
}
|
||||
```
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user