Compare commits

..

1 Commits

Author SHA1 Message Date
DevelopmentCats 85fe68275c feat(coder/modules/claude-code): testing claude-code path 2025-12-22 11:44:03 -06:00
40 changed files with 1304 additions and 2577 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.41.0
uses: crate-ci/typos@v1.40.0
with:
config: .github/typos.toml
validate-readme-files:
-1
View File
@@ -1,4 +1,3 @@
.DS_Store
# Logs
logs
*.log
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

-2
View File
@@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 913 B

@@ -0,0 +1,27 @@
{
"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,10 +6,6 @@ terraform {
coder = {
source = "coder/coder"
}
http = {
source = "hashicorp/http"
version = "~> 3.0"
}
}
}
@@ -21,24 +17,6 @@ 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"
@@ -46,18 +24,29 @@ data "coder_parameter" "hcloud_location" {
description = "Select the Hetzner Cloud location for your workspace."
type = "string"
default = "fsn1"
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
}
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"
}
}
@@ -120,47 +109,17 @@ resource "hcloud_volume_attachment" "home_volume_attachment" {
locals {
username = lower(data.coder_workspace_owner.me.name)
# --------------------
# 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
}
# 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
hcloud_server_type_options_for_selected_location = [
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
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
}
if contains(
meta.locations,
data.coder_parameter.hcloud_location.value
)
]
}
@@ -221,4 +180,4 @@ module "code-server" {
agent_id = coder_agent.main.id
order = 1
}
}
+3 -3
View File
@@ -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.4"
version = "1.0.3"
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.4"
version = "1.0.3"
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.4"
version = "1.0.3"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
+1 -1
View File
@@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
display_name = "tmux"
icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = base64encode(var.tmux_config)
TMUX_CONFIG = var.tmux_config
SAVE_INTERVAL = var.save_interval
})
run_on_start = true
+2 -2
View File
@@ -4,7 +4,7 @@ BOLD='\033[0;1m'
# Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
TMUX_CONFIG="${TMUX_CONFIG}"
# Function to install tmux
install_tmux() {
@@ -73,7 +73,7 @@ setup_tmux_config() {
mkdir -p "$config_dir"
if [ -n "$TMUX_CONFIG" ]; then
printf "%s" "$TMUX_CONFIG" > "$config_file"
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
@@ -1,7 +1,6 @@
#!/bin/bash
#set -o errexit
set -o errexit
set -o pipefail
set -x
port=${1:-3284}
@@ -11,30 +10,18 @@ port=${1:-3284}
agentapi_started=false
echo "Waiting for agentapi server to start on port $port..."
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
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)"
else
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
echo "agentapi server not responding ($i/15)"
continue 2
fi
done
agentapi_started=true
break
done
if [ "$agentapi_started" != "true" ]; then
@@ -1,7 +1,6 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -x
use_prompt=${1:-false}
port=${2:-3284}
+9 -3
View File
@@ -252,6 +252,12 @@ variable "compile_boundary_from_source" {
default = false
}
variable "claude_binary_path" {
type = string
description = "The path to add to PATH for the Claude binary."
default = "$HOME/.local/bin"
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
@@ -291,7 +297,7 @@ resource "coder_env" "disable_autoupdater" {
resource "coder_env" "claude_binary_path" {
agent_id = var.agent_id
name = "PATH"
value = "$HOME/.local/bin:$PATH"
value = "${var.claude_binary_path}:$PATH"
}
locals {
@@ -338,8 +344,8 @@ locals {
}
module "agentapi" {
source = "github.com/coder/registry//registry/coder/modules/agentapi?ref=cj%2Fagentapi%2Fdebug"
# version = "2.0.0"
source = "registry.coder.com/coder/agentapi/coder"
version = "2.0.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -1,7 +1,6 @@
#!/bin/bash
set -euo pipefail
set -x
BOLD='\033[0;1m'
@@ -1,7 +1,6 @@
#!/bin/bash
set -euo pipefail
set -x
command_exists() {
command -v "$1" > /dev/null 2>&1
+4 -4
View File
@@ -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.4"
version = "1.1.3"
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.4"
version = "1.1.3"
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.4"
version = "1.1.3"
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.4"
version = "1.1.3"
agent_id = coder_agent.main.id
agent_name = "main"
subdomain = false
+1 -1
View File
@@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
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,17 +1,9 @@
import { serve } from "bun";
import {
afterEach,
beforeAll,
describe,
expect,
it,
setDefaultTimeout,
} from "bun:test";
import { type Server, serve } from "bun";
import { describe, expect, it } from "bun:test";
import {
createJSONResponse,
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
@@ -19,48 +11,77 @@ import {
writeCoder,
} from "~test";
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);
}
}
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);
});
});
const setupContainer = async (
image = "lorello/alpine-bash",
vars: Record<string, string> = {},
) => {
const server = setupServer();
const server = await 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 = () => {
const fakeGithubHost = serve({
const setupServer = async (): Promise<Server> => {
let url: URL;
const fakeSlackHost = serve({
fetch: (req) => {
const url = new URL(req.url);
url = new URL(req.url);
if (url.pathname === "/api/v2/users/me/gitsshkey") {
return createJSONResponse({
public_key: "exists",
@@ -107,60 +128,5 @@ const setupServer = () => {
port: 0,
});
return fakeGithubHost;
return fakeSlackHost;
};
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);
});
});
+16 -8
View File
@@ -14,9 +14,10 @@ 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.3.0"
version = "1.2.1"
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
}
```
@@ -39,7 +40,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.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
@@ -52,7 +53,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
# Show parameter with limited options
@@ -66,7 +67,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
@@ -81,7 +82,7 @@ module "jetbrains" {
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -108,7 +109,7 @@ module "jetbrains" {
module "jetbrains_pycharm" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/workspace/project"
@@ -128,11 +129,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.3.0"
version = "1.2.1"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
}
```
@@ -169,6 +170,13 @@ 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 = "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" }
"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" }
}
}
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
}
assert {
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."])
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."])
error_message = "Expected coder_app tooltip to be set when provided"
}
}
run "tooltip_default_when_not_provided" {
run "tooltip_null_when_not_provided" {
command = plan
variables {
@@ -206,41 +206,8 @@ run "tooltip_default_when_not_provided" {
}
assert {
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}'"
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"
}
}
@@ -327,27 +294,3 @@ 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
+22 -33
View File
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
variable "tooltip" {
type = string
description = "Markdown text that is displayed when hovering over workspace apps."
default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
default = null
}
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}\\.[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'"
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"
}
}
@@ -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}${var.major_version == "latest" ? "&latest=true" : ""}"
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
}
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 = "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" },
"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" },
}
EOT
type = map(object({
@@ -149,15 +149,15 @@ variable "ide_config" {
build = string
}))
default = {
"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" }
"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" }
}
validation {
condition = length(var.ide_config) > 0
@@ -182,20 +182,6 @@ 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 => {
@@ -205,10 +191,13 @@ locals {
key = code
# Use API build number if available, otherwise fall back to ide_config build number
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
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
# Store API data for potential future use
json_data = local.selected_releases[code]
# 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
}
}
+6 -6
View File
@@ -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.6"
version = "1.0.5"
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.6"
version = "1.0.5"
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.6"
version = "1.0.5"
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.6"
version = "1.0.5"
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.6"
version = "1.0.5"
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.6"
version = "1.0.5"
agent_id = coder_agent.main.id
install = false
}
+5 -5
View File
@@ -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
+7 -7
View File
@@ -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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
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.1"
version = "1.1.0"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
enterprise = true
+31 -25
View File
@@ -7,34 +7,40 @@ INSTALL_DIR=${INSTALL_DIR}
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
ENTERPRISE=${ENTERPRISE}
# Fetch URL content to stdout
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
# Usage: fetch <url> [dest]
fetch() {
url="$1"
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
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
fi
}
dest="$${2:-}"
# 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}"
# 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
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
# shellcheck disable=SC2195
case "$${HTTP_CLIENT}" in
curl) curl -sSL --fail "$${url}" ;;
wget) wget -qO- "$${url}" ;;
busybox) busybox wget -qO- "$${url}" ;;
esac
fi
}
@@ -135,7 +141,7 @@ install() {
cd "$${TEMP_DIR}" || return 1
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
printf "Failed to download Vault.\n"
rm -rf "$${TEMP_DIR}"
return 1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

-15
View File
@@ -1,15 +0,0 @@
---
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
@@ -1,156 +0,0 @@
---
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)
@@ -1,35 +0,0 @@
#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
@@ -1,2 +0,0 @@
#!/bin/bash
sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}'
@@ -1,337 +0,0 @@
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"
}
@@ -1,450 +0,0 @@
[
{
"name": "COPARM1-2C-8G",
"cpu": 2,
"gpu": 0,
"ram": 8589934592,
"arch": "arm64"
},
{
"name": "COPARM1-4C-16G",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "arm64"
},
{
"name": "COPARM1-8C-32G",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "arm64"
},
{
"name": "COPARM1-16C-64G",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "arm64"
},
{
"name": "COPARM1-32C-128G",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "arm64"
},
{
"name": "DEV1-S",
"cpu": 2,
"gpu": 0,
"ram": 2147483648,
"arch": "x86_64"
},
{
"name": "DEV1-M",
"cpu": 3,
"gpu": 0,
"ram": 4294967296,
"arch": "x86_64"
},
{
"name": "DEV1-L",
"cpu": 4,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "DEV1-XL",
"cpu": 4,
"gpu": 0,
"ram": 12884901888,
"arch": "x86_64"
},
{
"name": "ENT1-XXS",
"cpu": 2,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "ENT1-XS",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "ENT1-S",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "ENT1-M",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "ENT1-L",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "ENT1-XL",
"cpu": 64,
"gpu": 0,
"ram": 274877906944,
"arch": "x86_64"
},
{
"name": "ENT1-2XL",
"cpu": 96,
"gpu": 0,
"ram": 412316860416,
"arch": "x86_64"
},
{
"name": "GP1-XS",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "GP1-S",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "GP1-M",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "GP1-L",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "GP1-XL",
"cpu": 48,
"gpu": 0,
"ram": 274877906944,
"arch": "x86_64"
},
{
"name": "L4-1-24G",
"cpu": 8,
"gpu": 1,
"ram": 51539607552,
"arch": "x86_64"
},
{
"name": "L4-2-24G",
"cpu": 16,
"gpu": 2,
"ram": 103079215104,
"arch": "x86_64"
},
{
"name": "L4-4-24G",
"cpu": 32,
"gpu": 4,
"ram": 206158430208,
"arch": "x86_64"
},
{
"name": "L4-8-24G",
"cpu": 64,
"gpu": 8,
"ram": 412316860416,
"arch": "x86_64"
},
{
"name": "PLAY2-PICO",
"cpu": 1,
"gpu": 0,
"ram": 2147483648,
"arch": "x86_64"
},
{
"name": "PLAY2-NANO",
"cpu": 2,
"gpu": 0,
"ram": 4294967296,
"arch": "x86_64"
},
{
"name": "PLAY2-MICRO",
"cpu": 4,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-HC-2C-4G",
"cpu": 2,
"gpu": 0,
"ram": 4294967296,
"arch": "x86_64"
},
{
"name": "POP2-2C-8G",
"cpu": 2,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-HM-2C-16G",
"cpu": 2,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "POP2-HC-4C-8G",
"cpu": 4,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-4C-16G",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "POP2-2C-8G-WIN",
"cpu": 2,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-HM-4C-32G",
"cpu": 4,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "POP2-HC-8C-16G",
"cpu": 8,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "POP2-HN-3",
"cpu": 2,
"gpu": 0,
"ram": 4294967296,
"arch": "x86_64"
},
{
"name": "POP2-8C-32G",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "POP2-4C-16G-WIN",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "POP2-HM-8C-64G",
"cpu": 8,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "POP2-HC-16C-32G",
"cpu": 16,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "POP2-HN-5",
"cpu": 4,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-16C-64G",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "POP2-8C-32G-WIN",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "POP2-HN-10",
"cpu": 4,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "POP2-HM-16C-128G",
"cpu": 16,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "POP2-HC-32C-64G",
"cpu": 32,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "POP2-32C-128G",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "POP2-HC-48C-96G",
"cpu": 48,
"gpu": 0,
"ram": 103079215104,
"arch": "x86_64"
},
{
"name": "POP2-16C-64G-WIN",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "POP2-HM-32C-256G",
"cpu": 32,
"gpu": 0,
"ram": 274877906944,
"arch": "x86_64"
},
{
"name": "POP2-HC-64C-128G",
"cpu": 64,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "POP2-48C-192G",
"cpu": 48,
"gpu": 0,
"ram": 206158430208,
"arch": "x86_64"
},
{
"name": "POP2-64C-256G",
"cpu": 64,
"gpu": 0,
"ram": 274877906944,
"arch": "x86_64"
},
{
"name": "POP2-HM-48C-384G",
"cpu": 48,
"gpu": 0,
"ram": 412316860416,
"arch": "x86_64"
},
{
"name": "POP2-32C-128G-WIN",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "POP2-HM-64C-512G",
"cpu": 64,
"gpu": 0,
"ram": 549755813888,
"arch": "x86_64"
},
{
"name": "PRO2-XXS",
"cpu": 2,
"gpu": 0,
"ram": 8589934592,
"arch": "x86_64"
},
{
"name": "PRO2-XS",
"cpu": 4,
"gpu": 0,
"ram": 17179869184,
"arch": "x86_64"
},
{
"name": "PRO2-S",
"cpu": 8,
"gpu": 0,
"ram": 34359738368,
"arch": "x86_64"
},
{
"name": "PRO2-M",
"cpu": 16,
"gpu": 0,
"ram": 68719476736,
"arch": "x86_64"
},
{
"name": "PRO2-L",
"cpu": 32,
"gpu": 0,
"ram": 137438953472,
"arch": "x86_64"
},
{
"name": "RENDER-S",
"cpu": 10,
"gpu": 1,
"ram": 45097156608,
"arch": "x86_64"
},
{
"name": "STARDUST1-S",
"cpu": 1,
"gpu": 0,
"ram": 1073741824,
"arch": "x86_64"
}
]
-41
View File
@@ -1,41 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 1.6 MiB

-14
View File
@@ -1,14 +0,0 @@
---
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.
@@ -1,73 +0,0 @@
---
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": "*"
}
```
@@ -1,120 +0,0 @@
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]
}
@@ -1,319 +0,0 @@
#!/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."
@@ -1,501 +0,0 @@
#!/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"
@@ -1,120 +0,0 @@
# --- 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
}