Skip to content

Commit c4c4840

Browse files
m4rrypromatifali
andauthored
feat: add digitalocean region module (#355)
Co-authored-by: Atif Ali <[email protected]>
1 parent 7e53098 commit c4c4840

File tree

3 files changed

+319
-0
lines changed

3 files changed

+319
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
display_name: DigitalOcean Region
3+
description: A parameter with human region names and icons
4+
icon: ../../../../.icons/digital-ocean.svg
5+
verified: true
6+
tags: [helper, parameter, digitalocean, regions]
7+
---
8+
9+
# DigitalOcean Region
10+
11+
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.
12+
13+
The simplest usage is:
14+
15+
```tf
16+
module "digitalocean-region" {
17+
count = data.coder_workspace.me.start_count
18+
source = "registry.coder.com/coder/digitalocean-region/coder"
19+
version = "1.0.0"
20+
default = "ams3"
21+
}
22+
```
23+
24+
## Examples
25+
26+
### Basic usage
27+
28+
```tf
29+
module "digitalocean-region" {
30+
count = data.coder_workspace.me.start_count
31+
source = "registry.coder.com/coder/digitalocean-region/coder"
32+
version = "1.0.0"
33+
}
34+
```
35+
36+
### With custom configuration
37+
38+
```tf
39+
module "digitalocean-region" {
40+
count = data.coder_workspace.me.start_count
41+
source = "registry.coder.com/coder/digitalocean-region/coder"
42+
version = "1.0.0"
43+
default = "ams3"
44+
mutable = true
45+
46+
custom_icons = {
47+
"ams3" = "/emojis/1f1f3-1f1f1.png"
48+
}
49+
50+
custom_names = {
51+
"ams3" = "Europe - Amsterdam (Primary)"
52+
}
53+
}
54+
```
55+
56+
### GPU-only toggle (internal parameter)
57+
58+
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.
59+
60+
## Available Regions
61+
62+
Refer to DigitalOcean’s official availability matrix for the most up-to-date information.
63+
64+
- GPU availability: currently only in `nyc2` and `tor1` (per DO docs). Others are non-GPU.
65+
- See: https://docs.digitalocean.com/platform/regional-availability/
66+
67+
### All datacenters (GPU status)
68+
69+
- `nyc2` - New York, United States (Legacy) - **GPU available**
70+
- `tor1` - Toronto, Canada - **GPU available**
71+
- `nyc3` - New York, United States
72+
- `ams3` - Amsterdam, Netherlands
73+
- `sfo3` - San Francisco, United States
74+
- `sgp1` - Singapore
75+
- `lon1` - London, United Kingdom
76+
- `fra1` - Frankfurt, Germany
77+
- `blr1` - Bangalore, India
78+
- `syd1` - Sydney, Australia
79+
- `atl1` - Atlanta, United States
80+
- `nyc1` - New York, United States (Legacy)
81+
- `sfo2` - San Francisco, United States (Legacy)
82+
- `sfo1` - San Francisco, United States (Legacy)
83+
- `ams2` - Amsterdam, Netherlands (Legacy)
84+
85+
## Associated template
86+
87+
Also see the Coder template registry for a [DigitalOcean Droplet template](https://registry.coder.com/templates/digitalocean-droplet) that provisions workspaces as DigitalOcean Droplets.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from "bun:test";
2+
import {
3+
runTerraformApply,
4+
runTerraformInit,
5+
testRequiredVariables,
6+
} from "~test";
7+
8+
describe("digitalocean-region", async () => {
9+
await runTerraformInit(import.meta.dir);
10+
11+
testRequiredVariables(import.meta.dir, {});
12+
13+
it("default output", async () => {
14+
const state = await runTerraformApply(import.meta.dir, {});
15+
expect(state.outputs.value.value).toBe("ams2");
16+
});
17+
18+
it("customized default", async () => {
19+
const state = await runTerraformApply(import.meta.dir, {
20+
regions: '["nyc1","ams3"]',
21+
default: "ams3",
22+
});
23+
expect(state.outputs.value.value).toBe("ams3");
24+
});
25+
26+
it("gpu only invalid default", async () => {
27+
const state = await runTerraformApply(import.meta.dir, {
28+
regions: '["nyc1"]',
29+
default: "nyc1",
30+
gpu_only: "true",
31+
});
32+
expect(state.outputs.value.value).toBe("nyc1");
33+
});
34+
35+
it("gpu only valid default", async () => {
36+
const state = await runTerraformApply(import.meta.dir, {
37+
regions: '["tor1"]',
38+
default: "tor1",
39+
gpu_only: "true",
40+
});
41+
expect(state.outputs.value.value).toBe("tor1");
42+
});
43+
44+
// Add more tests as needed for coder_parameter_order or other features
45+
});
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
4+
required_providers {
5+
coder = {
6+
source = "coder/coder"
7+
version = ">= 0.11"
8+
}
9+
}
10+
}
11+
12+
variable "display_name" {
13+
default = "DigitalOcean Region"
14+
description = "The display name of the parameter."
15+
type = string
16+
}
17+
18+
variable "description" {
19+
default = "The region to deploy workspace infrastructure."
20+
description = "The description of the parameter."
21+
type = string
22+
}
23+
24+
variable "default" {
25+
default = null
26+
description = "Default region"
27+
type = string
28+
}
29+
30+
31+
32+
variable "mutable" {
33+
default = false
34+
description = "Whether the parameter can be changed after creation."
35+
type = bool
36+
}
37+
38+
variable "custom_names" {
39+
default = {}
40+
description = "A map of custom display names for region IDs."
41+
type = map(string)
42+
}
43+
44+
variable "custom_icons" {
45+
default = {}
46+
description = "A map of custom icons for region IDs."
47+
type = map(string)
48+
}
49+
50+
variable "single_zone_per_region" {
51+
default = true
52+
description = "Whether to only include a single zone per region."
53+
type = bool
54+
}
55+
56+
variable "coder_parameter_order" {
57+
type = number
58+
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)."
59+
default = null
60+
}
61+
62+
data "coder_parameter" "gpu_only" {
63+
name = "digitalocean_gpu_only"
64+
display_name = "GPU-only regions"
65+
description = "Show only regions with GPUs"
66+
type = "bool"
67+
form_type = "checkbox"
68+
default = false
69+
mutable = var.mutable
70+
order = var.coder_parameter_order
71+
}
72+
73+
locals {
74+
zones = {
75+
# Active datacenters (recommended for new workloads)
76+
"nyc1" = {
77+
gpu = false
78+
name = "New York City, USA (NYC1)"
79+
icon = "/emojis/1f1fa-1f1f8.png"
80+
}
81+
"nyc3" = {
82+
gpu = false
83+
name = "New York City, USA (NYC3)"
84+
icon = "/emojis/1f1fa-1f1f8.png"
85+
}
86+
"ams3" = {
87+
gpu = false
88+
name = "Amsterdam, Netherlands"
89+
icon = "/emojis/1f1f3-1f1f1.png"
90+
}
91+
"sfo3" = {
92+
gpu = false
93+
name = "San Francisco, USA"
94+
icon = "/emojis/1f1fa-1f1f8.png"
95+
}
96+
"sgp1" = {
97+
gpu = false
98+
name = "Singapore"
99+
icon = "/emojis/1f1f8-1f1ec.png"
100+
}
101+
"lon1" = {
102+
gpu = false
103+
name = "London, United Kingdom"
104+
icon = "/emojis/1f1ec-1f1e7.png"
105+
}
106+
"fra1" = {
107+
gpu = false
108+
name = "Frankfurt, Germany"
109+
icon = "/emojis/1f1e9-1f1ea.png"
110+
}
111+
"tor1" = {
112+
gpu = true
113+
name = "Toronto, Canada"
114+
icon = "/emojis/1f1e8-1f1e6.png"
115+
}
116+
"blr1" = {
117+
gpu = false
118+
name = "Bangalore, India"
119+
icon = "/emojis/1f1ee-1f1f3.png"
120+
}
121+
"syd1" = {
122+
gpu = false
123+
name = "Sydney, Australia"
124+
icon = "/emojis/1f1e6-1f1fa.png"
125+
}
126+
"atl1" = {
127+
gpu = false
128+
name = "Atlanta, USA"
129+
icon = "/emojis/1f1fa-1f1f8.png"
130+
}
131+
# Legacy/Restricted datacenters (not recommended for new workloads)
132+
"nyc2" = {
133+
gpu = true # GPU available but restricted to existing users
134+
name = "New York City, USA (Legacy)"
135+
icon = "/emojis/1f1fa-1f1f8.png"
136+
}
137+
"sfo2" = {
138+
gpu = false # No GPU available per current regional availability
139+
name = "San Francisco, USA (Legacy SFO2)"
140+
icon = "/emojis/1f1fa-1f1f8.png"
141+
}
142+
"sfo1" = {
143+
gpu = false # No GPU in legacy datacenter
144+
name = "San Francisco, USA (Legacy SFO1)"
145+
icon = "/emojis/1f1fa-1f1f8.png"
146+
}
147+
"ams2" = {
148+
gpu = false # No GPU in legacy datacenter
149+
name = "Amsterdam, Netherlands (Legacy)"
150+
icon = "/emojis/1f1f3-1f1f1.png"
151+
}
152+
}
153+
}
154+
155+
locals {
156+
allowed_regions = data.coder_parameter.gpu_only.value ? [for k, v in local.zones : k if v.gpu] : keys(local.zones)
157+
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])
158+
}
159+
160+
data "coder_parameter" "region" {
161+
name = "digitalocean_region"
162+
display_name = var.display_name
163+
description = var.description
164+
icon = "/icon/digital-ocean.svg"
165+
mutable = var.mutable
166+
form_type = "radio"
167+
default = local.default_region
168+
order = var.coder_parameter_order
169+
dynamic "option" {
170+
for_each = {
171+
for k, v in local.zones : k => v
172+
if contains(local.allowed_regions, k)
173+
}
174+
content {
175+
icon = try(var.custom_icons[option.key], option.value.icon)
176+
name = try(var.custom_names[option.key], option.value.name)
177+
description = option.key
178+
value = option.key
179+
}
180+
}
181+
182+
183+
}
184+
output "value" {
185+
description = "DigitalOcean region identifier."
186+
value = data.coder_parameter.region.value
187+
}

0 commit comments

Comments
 (0)