mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dbdad101d | |||
| 2ecdbddf8f | |||
| fa8147e1bb | |||
| 4aa4448d81 | |||
| 3829a20756 | |||
| 15a16f279b | |||
| a6aca54728 | |||
| b2c1ff0770 | |||
| 41e86b8715 | |||
| 4f22907ed4 | |||
| 65ccc6ff48 | |||
| 2701dc09af | |||
| 60611ed593 | |||
| 7df0cb25c5 | |||
| cbb39bda6f | |||
| 99bd4a4139 | |||
| c819ca7f83 | |||
| accf5a34ab |
@@ -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:
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
---
|
||||
display_name: "Cloud DevOps Workspace"
|
||||
description: "A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP."
|
||||
icon: "https://raw.githubusercontent.com/coder/coder-icons/main/icons/cloud-devops.svg"
|
||||
tags:
|
||||
- devops
|
||||
- kubernetes
|
||||
- aws
|
||||
- eks
|
||||
- multi-cloud
|
||||
- terraform
|
||||
- cdk
|
||||
- pulumi
|
||||
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
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
# Run 'terraform test' from this template directory (where main.tf lives)
|
||||
|
||||
# --- Mock cloud providers so no external calls happen ---
|
||||
mock_provider "aws" {}
|
||||
mock_provider "kubernetes" {}
|
||||
|
||||
# Provide fake values for data sources your template reads
|
||||
override_data {
|
||||
target = data.aws_eks_cluster.eks
|
||||
values = {
|
||||
name = "unit-test-eks"
|
||||
endpoint = "https://example.eks.local"
|
||||
certificate_authority = [{
|
||||
data = base64encode("dummy-ca")
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.aws_eks_cluster_auth.eks
|
||||
values = {
|
||||
token = "dummy-token"
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------
|
||||
# 1) Validate configuration
|
||||
# ---------------------------
|
||||
run "validate" {
|
||||
command = validate
|
||||
}
|
||||
|
||||
# ---------------------------
|
||||
# 2) Plan with representative inputs
|
||||
# ---------------------------
|
||||
run "plan_with_defaults" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
host_cluster_name = "unit-test-eks"
|
||||
|
||||
# IaC/tooling toggles
|
||||
iac_tool = "terraform"
|
||||
enable_aws = true
|
||||
enable_azure = false
|
||||
enable_gcp = false
|
||||
|
||||
# Dev creds (empty OK for unit test)
|
||||
aws_access_key_id = ""
|
||||
aws_secret_access_key = ""
|
||||
azure_client_id = ""
|
||||
azure_tenant_id = ""
|
||||
azure_client_secret = ""
|
||||
gcp_service_account = ""
|
||||
}
|
||||
|
||||
# Simple sanity assertions (adjust resource addresses to your template)
|
||||
assert {
|
||||
condition = can(resource.kubernetes_namespace.workspace)
|
||||
error_message = "kubernetes_namespace.workspace was not created in plan."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(resource.coder_agent.main)
|
||||
error_message = "coder_agent.main was not planned."
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------
|
||||
# 3) Plan with CDK selected
|
||||
# ---------------------------
|
||||
run "plan_with_cdk" {
|
||||
command = plan
|
||||
variables {
|
||||
host_cluster_name = "unit-test-eks"
|
||||
iac_tool = "cdk"
|
||||
enable_aws = true
|
||||
enable_azure = false
|
||||
enable_gcp = false
|
||||
}
|
||||
|
||||
# Ensure the env reflects choice (string map lookup)
|
||||
assert {
|
||||
condition = contains(keys(resource.coder_agent.main.env), "IAC_TOOL")
|
||||
error_message = "IAC_TOOL env not present on coder_agent.main."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user