Skip to content

feat: add digitalocean region module #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions registry/umair/modules/digitalocean-region/README.md
Original file line number Diff line number Diff line change
@@ -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/coder/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/coder/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/coder/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 DigitalOcean’s 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.
46 changes: 46 additions & 0 deletions registry/umair/modules/digitalocean-region/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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
});
187 changes: 187 additions & 0 deletions registry/umair/modules/digitalocean-region/main.tf
Original file line number Diff line number Diff line change
@@ -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
}