Skip to content

feat: add icon and description fields to workspace preset #422

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

Merged
merged 7 commits into from
Jul 22, 2025
Merged
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
12 changes: 9 additions & 3 deletions docs/data-sources/workspace_preset.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ provider "coder" {}
# See the coder_parameter data source's documentation for examples of how to define
# parameters like the ones used below.
data "coder_workspace_preset" "example" {
name = "example"
name = "example"
description = "Example description of what this preset does."
icon = "/icon/example.svg"
parameters = {
(data.coder_parameter.example.name) = "us-central1-a"
(data.coder_parameter.ami.name) = "ami-xxxxxxxx"
Expand All @@ -30,8 +32,10 @@ data "coder_workspace_preset" "example" {

# Example of a default preset that will be pre-selected for users
data "coder_workspace_preset" "standard" {
name = "Standard"
default = true
name = "Standard"
description = "A workspace preset with medium compute in the US West region."
icon = "/icon/standard.svg"
default = true
parameters = {
(data.coder_parameter.instance_type.name) = "t3.medium"
(data.coder_parameter.region.name) = "us-west-2"
Expand All @@ -49,6 +53,8 @@ data "coder_workspace_preset" "standard" {
### Optional

- `default` (Boolean) Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default.
- `description` (String) Describe what this preset does.
- `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a built-in icon with `"${data.coder_workspace.me.access_url}/icon/<path>"`.
- `parameters` (Map of String) Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version.
- `prebuilds` (Block Set, Max: 1) Configuration for prebuilt workspaces associated with this preset. Coder will maintain a pool of standby workspaces based on this configuration. When a user creates a workspace using this preset, they are assigned a prebuilt workspace instead of waiting for a new one to build. See prebuilt workspace documentation [here](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces.md) (see [below for nested schema](#nestedblock--prebuilds))

Expand Down
10 changes: 7 additions & 3 deletions examples/data-sources/coder_workspace_preset/data-source.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ provider "coder" {}
# See the coder_parameter data source's documentation for examples of how to define
# parameters like the ones used below.
data "coder_workspace_preset" "example" {
name = "example"
name = "example"
description = "Example description of what this preset does."
icon = "/icon/example.svg"
parameters = {
(data.coder_parameter.example.name) = "us-central1-a"
(data.coder_parameter.ami.name) = "ami-xxxxxxxx"
Expand All @@ -15,8 +17,10 @@ data "coder_workspace_preset" "example" {

# Example of a default preset that will be pre-selected for users
data "coder_workspace_preset" "standard" {
name = "Standard"
default = true
name = "Standard"
description = "A workspace preset with medium compute in the US West region."
icon = "/icon/standard.svg"
default = true
parameters = {
(data.coder_parameter.instance_type.name) = "t3.medium"
(data.coder_parameter.region.name) = "us-west-2"
Expand Down
2 changes: 2 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ func TestIntegration(t *testing.T) {
"workspace_parameter.value": `param value`,
"workspace_parameter.icon": `param icon`,
"workspace_preset.name": `preset`,
"workspace_preset.description": `preset description`,
"workspace_preset.icon": `preset icon`,
"workspace_preset.default": `true`,
"workspace_preset.parameters.param": `preset param value`,
"workspace_preset.prebuilds.instances": `1`,
Expand Down
8 changes: 6 additions & 2 deletions integration/test-data-source/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ data "coder_parameter" "param" {
icon = "param icon"
}
data "coder_workspace_preset" "preset" {
name = "preset"
default = true
name = "preset"
description = "preset description"
icon = "preset icon"
Comment on lines +23 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we use rather realistic values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the naming convention used by the other parameters, as they follow a similar logic. Since this is an integration test, I don’t think adding realistic values here is necessary.

default = true
parameters = {
(data.coder_parameter.param.name) = "preset param value"
}
Expand Down Expand Up @@ -65,6 +67,8 @@ locals {
"workspace_parameter.value" : data.coder_parameter.param.value,
"workspace_parameter.icon" : data.coder_parameter.param.icon,
"workspace_preset.name" : data.coder_workspace_preset.preset.name,
"workspace_preset.description" : data.coder_workspace_preset.preset.description,
"workspace_preset.icon" : data.coder_workspace_preset.preset.icon,
"workspace_preset.default" : tostring(data.coder_workspace_preset.preset.default),
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
Expand Down
26 changes: 23 additions & 3 deletions provider/workspace_preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron.Dom | rbcron.Month | rbcron.Dow)

type WorkspacePreset struct {
Name string `mapstructure:"name"`
Default bool `mapstructure:"default"`
Parameters map[string]string `mapstructure:"parameters"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
Default bool `mapstructure:"default"`
Parameters map[string]string `mapstructure:"parameters"`
// There should always be only one prebuild block, but Terraform's type system
// still parses them as a slice, so we need to handle it as such. We could use
// an anonymous type and rd.Get to avoid a slice here, but that would not be possible
Expand Down Expand Up @@ -93,6 +95,24 @@ func workspacePresetDataSource() *schema.Resource {
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
"description": {
Copy link
Contributor Author

@ssncferreira ssncferreira Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied the schema definition for description and icon from the parameter schema (see https://github.com/coder/terraform-provider-coder/blob/main/provider/parameter.go#L184). Neither field currently has a size limit, but it might be good to add one to ensure the UI isn’t broken by overly large descriptions. Wdyt?

There is also a security consideration: we don’t seem to perform proper escaping or sanitization, which means it could be possible to inject HTML or JavaScript via the description. For the icon, since it’s used to fetch local files, we should be careful to prevent directory traversal attacks or other malicious paths.

Given that Coder typically runs on-premises, these risks might be lower, but it could still be a good practice to address them. Do we currently have any mechanisms in place to handle these concerns?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither field currently has a size limit

Do you mean in the provider, or in the database?

For the icon, since it’s used to fetch local files

What do you mean by 'local' here? The icon is used by the UI, and the icons are generally hosted on the control plane, although I've seen people link to icons on other domains.

There is also a security consideration: we don’t seem to perform proper escaping or sanitization

Where specifically have you checked?

Do we currently have any mechanisms in place to handle these concerns?

Template admins have a good bit of power in a Coder deployment, so there is some element of trust related to allowing users to create templates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean in the provider, or in the database?

In both:

I think it would make sense to add reasonable size limits to both description and icon, similar to what we do for templates table:

Wdyt?

What do you mean by 'local' here? The icon is used by the UI, and the icons are generally hosted on the control plane, although I've seen people link to icons on other domains.

By "local" I meant relative paths like /icon/region.svg. Given that, someone could technically enter a path like ../../etc/passwd or something similar.

Where specifically have you checked?

I tested the integration between the Terraform provider and Coder. I inserted a description with inline JavaScript and confirmed it was passed through and stored in the database. However, from my quick test, the UI seems to display the content as plain text and doesn’t interpret or render it as raw HTML, so that is good. I didn't test the icon path.

Template admins have a good bit of power in a Coder deployment, so there is some element of trust related to allowing users to create templates.

Totally agree. Given Coder is self-hosted and template creation is an admin-level task, I don't think this is a critical issue, more of an observation from working on this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to add reasonable size limits to both description and icon, similar to what we do for templates table

I think that's reasonable!

By "local" I meant relative paths like /icon/region.svg. Given that, someone could technically enter a path like ../../etc/passwd or something similar.

Right, but I don't think we would expose that in our static file server though. Might be no harm to sanitize the path though, good callout!

Type: schema.TypeString,
Optional: true,
Description: "Describe what this preset does.",
ValidateFunc: validation.StringLenBetween(0, 128),
},
"icon": {
Type: schema.TypeString,
Description: "A URL to an icon that will display in the dashboard. View built-in " +
"icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " +
"built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.",
ForceNew: true,
Optional: true,
ValidateFunc: validation.All(
helpers.ValidateURL,
validation.StringLenBetween(0, 256),
),
},
"default": {
Type: schema.TypeBool,
Description: "Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default.",
Expand Down
77 changes: 77 additions & 0 deletions provider/workspace_preset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ func TestWorkspacePreset(t *testing.T) {
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
description = <<-EOT
# Select the machine image
See the [registry](https://container.registry.blah/namespace) for options.
EOT
icon = "/icon/region.svg"
parameters = {
"region" = "us-east1-a"
}
Expand All @@ -34,6 +39,8 @@ func TestWorkspacePreset(t *testing.T) {
require.NotNil(t, resource)
attrs := resource.Primary.Attributes
require.Equal(t, attrs["name"], "preset_1")
require.Equal(t, attrs["description"], "# Select the machine image\nSee the [registry](https://container.registry.blah/namespace) for options.\n")
require.Equal(t, attrs["icon"], "/icon/region.svg")
require.Equal(t, attrs["parameters.region"], "us-east1-a")
return nil
},
Expand Down Expand Up @@ -76,6 +83,76 @@ func TestWorkspacePreset(t *testing.T) {
// So we test it here to make sure we don't regress.
ExpectError: regexp.MustCompile("Incorrect attribute value type"),
},
{
Name: "Description field is empty",
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
description = ""
parameters = {
"region" = "us-east1-a"
}
}`,
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
// So we test it here to make sure we don't regress.
ExpectError: nil,
},
{
Name: "Description field exceeds maximum supported length (128 characters)",
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vehicula leo sit amet mi laoreet, sed ornare velit tincidunt. Proin gravida lacinia blandit."
parameters = {
"region" = "us-east1-a"
}
}`,
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
// So we test it here to make sure we don't regress.
ExpectError: regexp.MustCompile(`expected length of description to be in the range \(0 - 128\)`),
},
{
Name: "Icon field is empty",
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
icon = ""
parameters = {
"region" = "us-east1-a"
}
}`,
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
// So we test it here to make sure we don't regress.
ExpectError: nil,
},
{
Name: "Icon field is an invalid URL",
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
icon = "/icon%.svg"
parameters = {
"region" = "us-east1-a"
}
}`,
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
// So we test it here to make sure we don't regress.
ExpectError: regexp.MustCompile("invalid URL escape"),
},
{
Name: "Icon field exceeds maximum supported length (256 characters)",
Config: `
data "coder_workspace_preset" "preset_1" {
name = "preset_1"
icon = "https://example.com/path/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.svg"
parameters = {
"region" = "us-east1-a"
}
}`,
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
// So we test it here to make sure we don't regress.
ExpectError: regexp.MustCompile(`expected length of icon to be in the range \(0 - 256\)`),
},
{
Name: "Parameters field is not provided",
Config: `
Expand Down
Loading