Compare commits

...

9 Commits

Author SHA1 Message Date
Atif Ali 9e47369905 chore: mark AMP CLI agent as verified (#408) 2025-09-03 04:37:22 +00:00
हिमांशु d9d44ca338 fix: bump versions of jfrog-oauth and jfrog-token (#407)
Closes #

## Description

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [x] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues
Follow up of #375
2025-09-01 22:31:43 +05:00
हिमांशु 7152b85246 feat: add conda support to JFrog modules (#375) 2025-09-01 21:54:23 +05:00
Atif Ali 41c6bece3e fix: use correct source url (#404) 2025-09-01 12:56:04 +00:00
Marcin Tojek 9452763f7d add: rstudio module (#327) 2025-09-01 14:25:28 +02:00
m4rrypro 77328656ff feat: add linode vm template (#367)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:07:54 +05:00
m4rrypro c4c484089f feat: add digitalocean region module (#355)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:06:08 +05:00
blink-so[bot] 7e53098bea Update jetbrains-gateway module references to coder/jetbrains (#396)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-29 17:46:25 +05:00
Lucas Kaplan 901043bb01 chore: Fixed path to avatar (#400)
This PR fixes a path issue in the README of the AJ0070 profile. The
original avatar path had the wrong extension.

---------

Co-authored-by: Benjamin <benjaminpeinhardt@gmail.com>
2025-08-28 18:56:11 -04:00
41 changed files with 1332 additions and 276 deletions
+1
View File
@@ -1,5 +1,6 @@
[default.extend-words]
muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Akamai</title>
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

@@ -2,7 +2,7 @@
display_name: Amp CLI
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: false
verified: true
tags: [agent, sourcegraph, amp, ai, tasks]
---
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.0.3"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -60,7 +60,7 @@ variable "sourcegraph_amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.0.3"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
@@ -24,6 +24,7 @@ module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder/projects"
install_claude_code = true
claude_code_version = "latest"
@@ -44,9 +45,10 @@ variable "anthropic_api_key" {
sensitive = true
}
resource "coder_env" "anthropic_api_key" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
}
# We are using presets to set the prompts, image, and set up instructions
@@ -174,19 +176,22 @@ data "coder_parameter" "preview_port" {
# Other variables for Claude Code
resource "coder_env" "claude_task_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
}
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
}
data "coder_provisioner" "me" {}
@@ -296,48 +301,42 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
agent_id = coder_agent.main.id
agent_name = "main"
order = 1
}
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
agent_name = "main"
}
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
agent_name = "main"
}
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
}
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder/projects"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder/projects"
}
resource "docker_volume" "home_volume" {
@@ -369,6 +368,7 @@ resource "docker_volume" "home_volume" {
resource "coder_app" "preview" {
agent_id = coder_agent.main.id
agent_name = "main"
slug = "preview"
display_name = "Preview your app"
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
@@ -422,4 +422,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

+4 -3
View File
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -26,6 +26,7 @@ module "jfrog" {
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
conda = ["conda", "conda-local"]
}
}
```
@@ -45,7 +46,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
@@ -74,7 +75,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -0,0 +1,6 @@
channels:
%{ for REPO in REPOS ~}
- https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/conda/${REPO}
%{ endfor ~}
- defaults
ssl_verify: true
@@ -126,4 +126,28 @@ EOF`;
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:@${fakeFrogApi}/conda/conda-main
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
- https://${user}:@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
});
@@ -58,6 +58,7 @@ variable "package_managers" {
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
conda = optional(list(string), [])
})
description = <<-EOF
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
@@ -67,6 +68,7 @@ variable "package_managers" {
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
}
EOF
}
@@ -98,6 +100,9 @@ locals {
pip_conf = templatefile(
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
)
conda_conf = templatefile(
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
)
}
data "coder_workspace" "me" {}
@@ -125,6 +130,9 @@ resource "coder_script" "jfrog" {
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
CONDA_CONF = local.conda_conf
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
}
))
run_on_start = true
+13
View File
@@ -81,6 +81,19 @@ else
fi
fi
# Configure conda to use the Artifactory "conda" repository.
if [ -z "${HAS_CONDA}" ]; then
not_configured conda
else
echo "🐍 Configuring conda..."
# Create conda config directory if it doesn't exist
mkdir -p ~/.conda
cat << EOF > ~/.condarc
${CONDA_CONF}
EOF
config_complete
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
+12 -8
View File
@@ -13,7 +13,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -22,6 +22,7 @@ module "jfrog" {
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
conda = ["conda", "conda-local"]
}
}
```
@@ -40,30 +41,33 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
npm = ["npm-local"]
go = ["go-local"]
pypi = ["pypi-local"]
npm = ["npm-local"]
go = ["go-local"]
pypi = ["pypi-local"]
conda = ["conda-local"]
}
}
```
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip` commands.
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip`, `conda` commands.
```shell
jf npm install prettier
jf go get github.com/golang/example/hello
jf pip install requests
conda install numpy
```
```shell
npm install prettier
go get github.com/golang/example/hello
pip install requests
conda install numpy
```
### Configure code-server with JFrog extension
@@ -73,7 +77,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -93,7 +97,7 @@ data "coder_workspace" "me" {}
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -0,0 +1,6 @@
channels:
%{ for REPO in REPOS ~}
- https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/conda/${REPO}
%{ endfor ~}
- defaults
ssl_verify: true
@@ -162,4 +162,29 @@ EOF`;
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
artifactory_access_token: "XXXX",
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:${token}@${fakeFrogApi}/conda/conda-main
- https://${user}:${token}@${fakeFrogApi}/conda/conda-secondary
- https://${user}:${token}@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
});
@@ -91,6 +91,7 @@ variable "package_managers" {
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
conda = optional(list(string), [])
})
description = <<-EOF
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
@@ -100,6 +101,7 @@ variable "package_managers" {
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
}
EOF
}
@@ -131,6 +133,9 @@ locals {
pip_conf = templatefile(
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
)
conda_conf = templatefile(
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
)
}
# Configure the Artifactory provider
@@ -171,6 +176,9 @@ resource "coder_script" "jfrog" {
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
CONDA_CONF = local.conda_conf
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
}
))
run_on_start = true
+13
View File
@@ -80,6 +80,19 @@ else
fi
fi
# Configure conda to use the Artifactory "conda" repository.
if [ -z "${HAS_CONDA}" ]; then
not_configured conda
else
echo "🐍 Configuring conda..."
# Create conda config directory if it doesn't exist
mkdir -p ~/.conda
cat << EOF > ~/.condarc
${CONDA_CONF}
EOF
config_complete
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
@@ -0,0 +1,25 @@
---
display_name: RStudio Server
description: Deploy the Rocker Project distribution of RStudio Server in your Coder workspace.
icon: ../../../../.icons/rstudio.svg
verified: true
tags: [rstudio, ide, web]
---
# RStudio Server
> [!NOTE]
> This module requires `docker` to be available in the workspace. Check [Docker in Workspaces](https://coder.com/docs/admin/templates/extending-templates/docker-in-workspaces) to learn how you can set it up.
Deploy the Rocker Project distribution of RStudio Server in your Coder workspace.
![RStudio Server](../../.images/rstudio-server.png)
```tf
module "rstudio-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/rstudio-server/coder"
version = "0.9.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,123 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "docker_socket" {
type = string
description = "(Optional) Docker socket URI"
default = ""
}
variable "rstudio_server_version" {
type = string
description = "RStudio Server version"
default = "4.5.1"
}
variable "disable_auth" {
type = bool
description = "Disable auth"
default = true
}
variable "rstudio_user" {
type = string
description = "RStudio user"
default = "rstudio"
sensitive = true
}
variable "rstudio_password" {
type = string
description = "RStudio password"
default = "rstudio"
sensitive = true
}
variable "project_path" {
type = string
description = "The path to RStudio project, it will be mounted in the container."
default = null
}
variable "port" {
type = number
description = "The port to run rstudio-server on."
default = 8787
}
variable "enable_renv" {
type = bool
description = "If renv.lock exists, renv will restore the environment and install dependencies"
default = true
}
variable "renv_cache_volume" {
type = string
description = "The name of the volume used by Renv to preserve dependencies between container restarts"
default = "renv-cache-volume"
}
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "rstudio-server" {
agent_id = var.agent_id
display_name = "rstudio-server"
icon = "/icon/rstudio.svg"
script = templatefile("${path.module}/run.sh", {
DOCKER_HOST : var.docker_socket,
SERVER_VERSION : var.rstudio_server_version,
DISABLE_AUTH : var.disable_auth,
RSTUDIO_USER : var.rstudio_user,
RSTUDIO_PASSWORD : var.rstudio_password,
PROJECT_PATH : var.project_path,
PORT : var.port,
ENABLE_RENV : var.enable_renv,
RENV_CACHE_VOLUME : var.renv_cache_volume,
})
run_on_start = true
}
resource "coder_app" "rstudio-server" {
agent_id = var.agent_id
slug = "rstudio-server"
display_name = "RStudio Server"
url = "http://localhost:${var.port}"
icon = "/icon/rstudio.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,57 @@
#!/usr/bin/env sh
set -eu
BOLD='\033[0;1m'
RESET='\033[0m'
printf "$${BOLD}Starting RStudio Server (Rocker)...$${RESET}\n"
# Wait for docker to become ready
max_attempts=10
delay=2
attempt=1
while ! docker ps; do
if [ $attempt -ge $max_attempts ]; then
echo "Failed to list containers after $${max_attempts} attempts."
exit 1
fi
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
sleep $delay
attempt=$(expr "$attempt" + 1)
delay=$(expr "$delay" \* 2) # exponential backoff
done
# Pull the specified version
IMAGE="rocker/rstudio:${SERVER_VERSION}"
docker pull "$${IMAGE}"
# Create (or reuse) a persistent renv cache volume
docker volume create "${RENV_CACHE_VOLUME}"
# Run container (auto-remove on stop)
docker run -d --rm \
--name rstudio-server \
-p "${PORT}:8787" \
-e DISABLE_AUTH="${DISABLE_AUTH}" \
-e USER="${RSTUDIO_USER}" \
-e PASSWORD="${RSTUDIO_PASSWORD}" \
-e RENV_PATHS_CACHE="/renv/cache" \
-v "${PROJECT_PATH}:/home/${RSTUDIO_USER}/project" \
-v "${RENV_CACHE_VOLUME}:/renv/cache" \
"$${IMAGE}"
# Make RENV_CACHE_VOLUME writable to USER
docker exec rstudio-server bash -c 'chmod -R 0777 /renv/cache'
# Optional renv restore
if [ "${ENABLE_RENV}" = "true" ] && [ -f "${PROJECT_PATH}/renv.lock" ]; then
echo "Restoring R environment via renv..."
docker exec -u "${RSTUDIO_USER}" rstudio-server R -q -e \
'if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv", repos="https://cloud.r-project.org"); renv::restore(prompt = FALSE)'
fi
[ "${DISABLE_AUTH}" != "true" ] && echo "User: ${RSTUDIO_USER}"
printf "\n$${BOLD}RStudio Server ${SERVER_VERSION} is running on port ${PORT}$${RESET}\n"
@@ -326,6 +326,7 @@ module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
}
+12 -21
View File
@@ -201,28 +201,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
order = 1
}
# See https://registry.coder.com/modules/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
folder = "/home/coder"
}
locals {
@@ -293,4 +284,4 @@ resource "coder_metadata" "workspace_info" {
resource "aws_ec2_instance_state" "dev" {
instance_id = aws_instance.dev.id
state = data.coder_workspace.me.transition == "start" ? "running" : "stopped"
}
}
+12 -21
View File
@@ -144,28 +144,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
locals {
@@ -322,4 +313,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${data.coder_parameter.home_size.value} GiB"
}
}
}
@@ -37,6 +37,7 @@ module "windows_rdp" {
admin_password = random_password.admin_password.result
agent_id = resource.coder_agent.main.id
agent_name = "main"
resource_id = null # Unused, to be removed in a future version
}
@@ -272,28 +272,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "digitalocean_volume" "home_volume" {
@@ -358,4 +349,4 @@ resource "coder_metadata" "volume-info" {
key = "size"
value = "${digitalocean_volume.home_volume.size} GiB"
}
}
}
@@ -330,28 +330,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/workspaces"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/workspaces"
}
resource "coder_metadata" "container_info" {
@@ -369,4 +360,4 @@ resource "coder_metadata" "container_info" {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
}
+12 -21
View File
@@ -129,28 +129,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "docker_volume" "home_volume" {
@@ -217,4 +208,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
@@ -291,28 +291,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/workspaces"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/workspaces"
}
# Create metadata for the workspace and home disk.
@@ -338,4 +329,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
}
+12 -21
View File
@@ -99,28 +99,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "google_compute_instance" "dev" {
@@ -181,4 +172,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
}
@@ -52,28 +52,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
# See https://registry.terraform.io/modules/terraform-google-modules/container-vm
@@ -122,6 +113,7 @@ resource "google_compute_instance" "dev" {
resource "coder_agent_instance" "dev" {
count = data.coder_workspace.me.start_count
agent_id = coder_agent.main.id
agent_name = "main"
instance_id = google_compute_instance.dev[0].instance_id
}
@@ -133,4 +125,4 @@ resource "coder_metadata" "workspace_info" {
key = "image"
value = module.gce-container.container.image
}
}
}
@@ -422,28 +422,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "coder_metadata" "container_info" {
@@ -461,4 +452,4 @@ resource "coder_metadata" "container_info" {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
}
@@ -106,28 +106,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "kubernetes_persistent_volume_claim" "home" {
@@ -319,4 +310,4 @@ resource "kubernetes_pod" "main" {
}
}
}
}
}
@@ -177,6 +177,7 @@ resource "coder_agent" "main" {
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
agent_name = "main"
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
@@ -118,8 +118,9 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
agent_id = coder_agent.main.id
agent_name = "main"
order = 1
}
locals {
+7 -4
View File
@@ -34,9 +34,10 @@ resource "coder_agent" "main" {
# Use this to set environment variables in your workspace
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env
resource "coder_env" "welcome_message" {
agent_id = coder_agent.main.id
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
agent_id = coder_agent.main.id
agent_name = "main"
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
}
# Adds code-server
@@ -48,13 +49,15 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_id = coder_agent.main.id
agent_name = "main"
}
# Runs a script at workspace start/stop or on a cron schedule
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
resource "coder_script" "startup_script" {
agent_id = coder_agent.main.id
agent_name = "main"
display_name = "Startup Script"
script = <<-EOF
#!/bin/sh
@@ -0,0 +1,87 @@
---
display_name: DigitalOcean Region
description: A parameter with human region names and icons
icon: ../../../../.icons/digital-ocean.svg
verified: true
tags: [helper, parameter, digitalocean, regions]
---
# DigitalOcean Region
This module adds DigitalOcean regions to your Coder template with automatic GPU filtering. You can customize display names and icons using the `custom_names` and `custom_icons` arguments.
The simplest usage is:
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
default = "ams3"
}
```
## Examples
### Basic usage
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
}
```
### With custom configuration
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
default = "ams3"
mutable = true
custom_icons = {
"ams3" = "/emojis/1f1f3-1f1f1.png"
}
custom_names = {
"ams3" = "Europe - Amsterdam (Primary)"
}
}
```
### GPU-only toggle (internal parameter)
This module automatically exposes a "GPU-only regions" checkbox in the template UI. When checked, it shows only GPU-capable regions and auto-selects the first one. When unchecked, it shows all available regions.
## Available Regions
Refer to DigitalOceans official availability matrix for the most up-to-date information.
- GPU availability: currently only in `nyc2` and `tor1` (per DO docs). Others are non-GPU.
- See: https://docs.digitalocean.com/platform/regional-availability/
### All datacenters (GPU status)
- `nyc2` - New York, United States (Legacy) - **GPU available**
- `tor1` - Toronto, Canada - **GPU available**
- `nyc3` - New York, United States
- `ams3` - Amsterdam, Netherlands
- `sfo3` - San Francisco, United States
- `sgp1` - Singapore
- `lon1` - London, United Kingdom
- `fra1` - Frankfurt, Germany
- `blr1` - Bangalore, India
- `syd1` - Sydney, Australia
- `atl1` - Atlanta, United States
- `nyc1` - New York, United States (Legacy)
- `sfo2` - San Francisco, United States (Legacy)
- `sfo1` - San Francisco, United States (Legacy)
- `ams2` - Amsterdam, Netherlands (Legacy)
## Associated template
Also see the Coder template registry for a [DigitalOcean Droplet template](https://registry.coder.com/templates/digitalocean-droplet) that provisions workspaces as DigitalOcean Droplets.
@@ -0,0 +1,45 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("digitalocean-region", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("ams2");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["nyc1","ams3"]',
default: "ams3",
});
expect(state.outputs.value.value).toBe("ams3");
});
it("gpu only invalid default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["nyc1"]',
default: "nyc1",
gpu_only: "true",
});
expect(state.outputs.value.value).toBe("nyc1");
});
it("gpu only valid default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["tor1"]',
default: "tor1",
gpu_only: "true",
});
expect(state.outputs.value.value).toBe("tor1");
});
// Add more tests as needed for coder_parameter_order or other features
});
@@ -0,0 +1,187 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.11"
}
}
}
variable "display_name" {
default = "DigitalOcean Region"
description = "The display name of the parameter."
type = string
}
variable "description" {
default = "The region to deploy workspace infrastructure."
description = "The description of the parameter."
type = string
}
variable "default" {
default = null
description = "Default region"
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for region IDs."
type = map(string)
}
variable "custom_icons" {
default = {}
description = "A map of custom icons for region IDs."
type = map(string)
}
variable "single_zone_per_region" {
default = true
description = "Whether to only include a single zone per region."
type = bool
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_parameter" "gpu_only" {
name = "digitalocean_gpu_only"
display_name = "GPU-only regions"
description = "Show only regions with GPUs"
type = "bool"
form_type = "checkbox"
default = false
mutable = var.mutable
order = var.coder_parameter_order
}
locals {
zones = {
# Active datacenters (recommended for new workloads)
"nyc1" = {
gpu = false
name = "New York City, USA (NYC1)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"nyc3" = {
gpu = false
name = "New York City, USA (NYC3)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"ams3" = {
gpu = false
name = "Amsterdam, Netherlands"
icon = "/emojis/1f1f3-1f1f1.png"
}
"sfo3" = {
gpu = false
name = "San Francisco, USA"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sgp1" = {
gpu = false
name = "Singapore"
icon = "/emojis/1f1f8-1f1ec.png"
}
"lon1" = {
gpu = false
name = "London, United Kingdom"
icon = "/emojis/1f1ec-1f1e7.png"
}
"fra1" = {
gpu = false
name = "Frankfurt, Germany"
icon = "/emojis/1f1e9-1f1ea.png"
}
"tor1" = {
gpu = true
name = "Toronto, Canada"
icon = "/emojis/1f1e8-1f1e6.png"
}
"blr1" = {
gpu = false
name = "Bangalore, India"
icon = "/emojis/1f1ee-1f1f3.png"
}
"syd1" = {
gpu = false
name = "Sydney, Australia"
icon = "/emojis/1f1e6-1f1fa.png"
}
"atl1" = {
gpu = false
name = "Atlanta, USA"
icon = "/emojis/1f1fa-1f1f8.png"
}
# Legacy/Restricted datacenters (not recommended for new workloads)
"nyc2" = {
gpu = true # GPU available but restricted to existing users
name = "New York City, USA (Legacy)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sfo2" = {
gpu = false # No GPU available per current regional availability
name = "San Francisco, USA (Legacy SFO2)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sfo1" = {
gpu = false # No GPU in legacy datacenter
name = "San Francisco, USA (Legacy SFO1)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"ams2" = {
gpu = false # No GPU in legacy datacenter
name = "Amsterdam, Netherlands (Legacy)"
icon = "/emojis/1f1f3-1f1f1.png"
}
}
}
locals {
allowed_regions = data.coder_parameter.gpu_only.value ? [for k, v in local.zones : k if v.gpu] : keys(local.zones)
default_region = data.coder_parameter.gpu_only.value ? (length([for k, v in local.zones : k if v.gpu]) > 0 ? [for k, v in local.zones : k if v.gpu][0] : null) : (var.default != null && var.default != "" ? var.default : keys(local.zones)[0])
}
data "coder_parameter" "region" {
name = "digitalocean_region"
display_name = var.display_name
description = var.description
icon = "/icon/digital-ocean.svg"
mutable = var.mutable
form_type = "radio"
default = local.default_region
order = var.coder_parameter_order
dynamic "option" {
for_each = {
for k, v in local.zones : k => v
if contains(local.allowed_regions, k)
}
content {
icon = try(var.custom_icons[option.key], option.value.icon)
name = try(var.custom_names[option.key], option.value.name)
description = option.key
value = option.key
}
}
}
output "value" {
description = "DigitalOcean region identifier."
value = data.coder_parameter.region.value
}
@@ -0,0 +1,47 @@
---
display_name: Linode Instance (Linux)
description: Provision Linode instances as Coder workspaces
icon: ../../../../.icons/akamai.svg
verified: false
tags: [vm, linux, linode]
---
# Remote Development on Linode Instances
Provision Linode instances as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
To deploy workspaces as Linode instances, you'll need:
- Linode [personal access token (PAT)](https://www.linode.com/docs/products/tools/api/guides/manage-api-tokens/)
### Authentication
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Linode.
Obtain a [Linode Personal Access Token](https://cloud.linode.com/profile/tokens) and set the `linode_token` variable when deploying the template.
For other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/linode/linode/latest/docs).
## Features
- **Multiple Instance Types**: From Nanode 1GB to 32GB configurations
- **Comprehensive OS Support**: Ubuntu, Debian, CentOS, Fedora, AlmaLinux, Rocky Linux
- **Global Regions**: 32 Linode regions across North America, Europe, Asia-Pacific, South America, and Australia
- **Persistent Storage**: Configurable volumes (10GB-1TB) that persist `$HOME` across workspace restarts
- **Development Tools**: Pre-configured with VS Code Server
- **Monitoring**: Built-in CPU, memory, and disk usage monitoring
## Architecture
This template provisions the following resources:
- Linode instance (ephemeral, deleted on stop)
- Linode volume (persistent, mounted to `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
> [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,56 @@
#cloud-config
hostname: ${hostname}
users:
- name: ${username}
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
groups: sudo
shell: /bin/bash
packages:
- git
- curl
- wget
- unzip
disk_setup:
/dev/sdb:
table_type: 'gpt'
layout: true
overwrite: false
fs_setup:
- label: ${home_volume_label}
filesystem: ext4
device: /dev/sdb
partition: auto
mounts:
- ["/dev/sdb", "/home/${username}", "ext4", "defaults", "0", "2"]
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${init_script}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${username}
ExecStart=/opt/coder/init
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-1000
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
- mkdir -p /home/${username}
- chown ${username}:${username} /home/${username}
- systemctl enable coder-agent
- systemctl start coder-agent
+397
View File
@@ -0,0 +1,397 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
linode = {
source = "linode/linode"
}
}
}
provider "coder" {}
# Variable for Linode API token
variable "linode_token" {
description = "Linode API token for authentication"
type = string
sensitive = true
default = ""
}
# Configure the Linode Provider
provider "linode" {
token = var.linode_token != "" ? var.linode_token : null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
metadata {
key = "home"
display_name = "Home Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}"
}
}
locals {
vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
root_disk_label = substr("${local.vm_name}-root", 0, 32)
home_volume_label = substr("${local.vm_name}-home", 0, 32)
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
data "coder_parameter" "region" {
name = "region"
display_name = "Region"
description = "This is the region where your workspace will be created."
icon = "/emojis/1f30e.png"
type = "string"
default = "us-east"
mutable = false
option {
name = "Newark, NJ (US East)"
value = "us-east"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Washington, DC (US East)"
value = "us-iad"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Fremont, CA (US West)"
value = "us-west"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Los Angeles, CA (US West)"
value = "us-lax"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Dallas, TX (US Central)"
value = "us-central"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Chicago, IL (US Central)"
value = "us-ord"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Atlanta, GA (US Southeast)"
value = "us-southeast"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Miami, FL (US Southeast)"
value = "us-mia"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Seattle, WA (US West)"
value = "us-sea"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Toronto, CA"
value = "ca-central"
icon = "/emojis/1f1e8-1f1e6.png"
}
option {
name = "London, UK"
value = "eu-west"
icon = "/emojis/1f1ec-1f1e7.png"
}
option {
name = "London 2, UK"
value = "gb-lon"
icon = "/emojis/1f1ec-1f1e7.png"
}
option {
name = "Frankfurt, DE"
value = "eu-central"
icon = "/emojis/1f1e9-1f1ea.png"
}
option {
name = "Frankfurt 2, DE"
value = "de-fra-2"
icon = "/emojis/1f1e9-1f1ea.png"
}
option {
name = "Paris, FR"
value = "fr-par"
icon = "/emojis/1f1eb-1f1f7.png"
}
option {
name = "Amsterdam, NL"
value = "nl-ams"
icon = "/emojis/1f1f3-1f1f1.png"
}
option {
name = "Stockholm, SE"
value = "se-sto"
icon = "/emojis/1f1f8-1f1ea.png"
}
option {
name = "Madrid, ES"
value = "es-mad"
icon = "/emojis/1f1ea-1f1f8.png"
}
option {
name = "Milan, IT"
value = "it-mil"
icon = "/emojis/1f1ee-1f1f9.png"
}
option {
name = "Singapore, SG"
value = "ap-south"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Singapore 2, SG"
value = "sg-sin-2"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Tokyo 2, JP"
value = "ap-northeast"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Tokyo 3, JP"
value = "jp-tyo-3"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Osaka, JP"
value = "jp-osa"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Sydney, AU"
value = "ap-southeast"
icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Melbourne, AU"
value = "au-mel"
icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Mumbai, IN"
value = "ap-west"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Mumbai 2, IN"
value = "in-bom-2"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Chennai, IN"
value = "in-maa"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Jakarta, ID"
value = "id-cgk"
icon = "/emojis/1f1ee-1f1e9.png"
}
option {
name = "Sao Paulo, BR"
value = "br-gru"
icon = "/emojis/1f1e7-1f1f7.png"
}
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance Type"
description = "Which Linode instance type would you like to use?"
default = "g6-nanode-1"
type = "string"
icon = "/icon/memory.svg"
mutable = false
option {
name = "Nanode 1GB (1 vCPU, 1 GB RAM)"
value = "g6-nanode-1"
}
option {
name = "Linode 2GB (1 vCPU, 2 GB RAM)"
value = "g6-standard-1"
}
option {
name = "Linode 4GB (2 vCPU, 4 GB RAM)"
value = "g6-standard-2"
}
option {
name = "Linode 8GB (4 vCPU, 8 GB RAM)"
value = "g6-standard-4"
}
option {
name = "Linode 16GB (6 vCPU, 16 GB RAM)"
value = "g6-standard-6"
}
option {
name = "Linode 32GB (8 vCPU, 32 GB RAM)"
value = "g6-standard-8"
}
}
data "coder_parameter" "instance_image" {
name = "instance_image"
display_name = "Instance Image"
description = "Which Linode image would you like to use?"
default = "linode/ubuntu24.04"
type = "string"
mutable = false
option {
name = "Ubuntu 24.04 LTS"
value = "linode/ubuntu24.04"
icon = "/icon/ubuntu.svg"
}
option {
name = "Debian 13"
value = "linode/debian13"
icon = "/icon/debian.svg"
}
option {
name = "Fedora 42"
value = "linode/fedora42"
icon = "/icon/fedora.svg"
}
option {
name = "AlmaLinux 9"
value = "linode/almalinux9"
icon = "/icon/almalinux.svg"
}
option {
name = "Rocky Linux 9"
value = "linode/rocky9"
icon = "/icon/rockylinux.svg"
}
}
data "coder_parameter" "home_volume_size" {
name = "home_volume_size"
display_name = "Home Volume Size (GB)"
description = "How large would you like your home volume to be (in GB)?"
type = "number"
default = 20
mutable = true
validation {
min = 10
max = 1024
monotonic = "increasing"
}
}
resource "linode_volume" "home_volume" {
label = local.home_volume_label
size = data.coder_parameter.home_volume_size.value
region = data.coder_parameter.region.value
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
}
resource "linode_instance" "workspace" {
count = data.coder_workspace.me.start_count
label = local.vm_name
region = data.coder_parameter.region.value
type = data.coder_parameter.instance_type.value
private_ip = true
metadata {
user_data = base64encode(templatefile("cloud-init/cloud-config.yaml.tftpl", {
hostname = local.vm_name
username = lower(data.coder_workspace_owner.me.name)
home_volume_label = linode_volume.home_volume.label
init_script = base64encode(coder_agent.main.init_script)
coder_agent_token = coder_agent.main.token
}))
}
tags = ["coder", "workspace", lower(data.coder_workspace_owner.me.name), lower(data.coder_workspace.me.name)]
}
# Create root disk
resource "linode_instance_disk" "root" {
count = data.coder_workspace.me.start_count
label = "boot"
linode_id = linode_instance.workspace[0].id
size = 25000 # 25GB boot disk
image = data.coder_parameter.instance_image.value
}
# Create instance configuration with volume attached
resource "linode_instance_config" "workspace" {
count = data.coder_workspace.me.start_count
label = "${local.vm_name}-config"
linode_id = linode_instance.workspace[0].id
device {
device_name = "sda"
disk_id = linode_instance_disk.root[0].id
}
device {
device_name = "sdb"
volume_id = linode_volume.home_volume.id
}
root_device = "/dev/sda"
kernel = "linode/latest-64bit"
booted = true
}
resource "coder_metadata" "workspace-info" {
count = data.coder_workspace.me.start_count
resource_id = linode_instance.workspace[0].id
item {
key = "region"
value = linode_instance.workspace[0].region
}
item {
key = "type"
value = linode_instance.workspace[0].type
}
}