diff --git a/registry/coder/.images/aws-ec2-instance-type.png b/registry/coder/.images/aws-ec2-instance-type.png new file mode 100644 index 000000000..fdb3175bd Binary files /dev/null and b/registry/coder/.images/aws-ec2-instance-type.png differ diff --git a/registry/coder/modules/aws-ec2-instance-type/.scripts/update.sh b/registry/coder/modules/aws-ec2-instance-type/.scripts/update.sh new file mode 100755 index 000000000..ea440d2ef --- /dev/null +++ b/registry/coder/modules/aws-ec2-instance-type/.scripts/update.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# Regenerates instance-types.json from the AWS EC2 API. Edit the families list +# below, then run this script and commit the result. Sizes within each family +# are enumerated from AWS, so a newly released size is picked up automatically. +# +# Requires the AWS CLI with credentials allowed to call +# ec2:DescribeInstanceTypes. Override the query region with AWS_REGION +# (default us-east-1, which offers every family in the list). + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +output_file="$script_dir/../instance-types.json" +region="${AWS_REGION:-us-east-1}" + +# Instance families to offer. general: t3/t4g/m5/m7g, compute: c5, memory: r5, +# storage: i3, gpu: g4dn. +families=(t3 t4g m5 m7g c5 r5 i3 g4dn) + +if ! command -v aws > /dev/null 2>&1; then + echo "error: aws CLI not found; install and configure it first" >&2 + exit 1 +fi + +# Turn the families into an instance-type wildcard filter, e.g. "t3.*,m5.*". +family_filter="" +for family in "${families[@]}"; do + family_filter="${family_filter:+$family_filter,}$family.*" +done + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +# bare-metal=false keeps the catalog to virtualized sizes; .metal instances are +# not sensible workspace defaults. The AWS CLI paginates automatically. +aws ec2 describe-instance-types \ + --region "$region" \ + --filters "Name=instance-type,Values=${family_filter}" "Name=bare-metal,Values=false" \ + --output json \ + --query 'sort_by(InstanceTypes, &MemoryInfo.SizeInMiB)[].{type: InstanceType, vcpus: VCpuInfo.DefaultVCpus, memory_mib: MemoryInfo.SizeInMiB, gpus: (GpuInfo.Gpus[0].Count || `0`), arch: ProcessorInfo.SupportedArchitectures[-1]}' \ + > "$tmp" + +mv "$tmp" "$output_file" + +# AWS CLI emits 4-space JSON; match the repo's prettier formatting. +if command -v bun > /dev/null 2>&1; then + bun x prettier --write "$output_file" > /dev/null +else + echo "note: bun not found; run 'bun run fmt' before committing $output_file" >&2 +fi + +echo "Updated $output_file" diff --git a/registry/coder/modules/aws-ec2-instance-type/README.md b/registry/coder/modules/aws-ec2-instance-type/README.md new file mode 100644 index 000000000..024bab6fc --- /dev/null +++ b/registry/coder/modules/aws-ec2-instance-type/README.md @@ -0,0 +1,121 @@ +--- +display_name: AWS EC2 Instance Type +description: A parameter with human readable AWS EC2 instance type names +icon: ../../../../.icons/aws.svg +verified: false +tags: [helper, parameter, instances, aws] +--- + +# AWS EC2 Instance Type + +A parameter with common AWS EC2 instance types, filtered by instance family. +Each option is labeled with its vCPU, RAM, and architecture so developers can +select the machine that best fits their workspace. + +```tf +module "aws_ec2_instance_type" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-ec2-instance-type/coder" + version = "1.0.0" + default = "t3.micro" +} + +resource "aws_instance" "dev" { + instance_type = module.aws_ec2_instance_type[0].value + # ... +} +``` + +![AWS EC2 Instance Type parameter](../../.images/aws-ec2-instance-type.png) + +## Examples + +### Choose instance families + +By default only the `t3` family is offered. Pass `include` to expose more +families and preselect one: + +```tf +module "aws_ec2_instance_type" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-ec2-instance-type/coder" + version = "1.0.0" + include = ["t3", "m5", "c5"] + default = "m5.large" +} +``` + +The bundled catalog covers the `t3`, `t4g`, `m5`, `m7g`, `c5`, `r5`, `i3`, and +`g4dn` families. + +### Customize labels and tooltips + +Each option is labeled with its specs and architecture (for example +`2 vCPU, 4 GiB RAM (amd64)`) and shows the instance type as a tooltip. Override +either per instance type: + +```tf +module "aws_ec2_instance_type" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/aws-ec2-instance-type/coder" + version = "1.0.0" + default = "t3.medium" + + custom_metadata = { + "t3.medium" = { + name = "Standard workspace" + description = "Recommended default" + } + } +} +``` + +### Architecture-aware provisioning + +The `instances` output maps every instance type ID to its metadata, including architecture fields. Look up the selected value to configure the agent and pick a matching AMI: + +```tf +module "aws_ec2_instance_type" { + source = "registry.coder.com/coder/aws-ec2-instance-type/coder" + version = "1.0.0" +} + +locals { + selected = module.aws_ec2_instance_type.instances[module.aws_ec2_instance_type.value] +} + +resource "coder_agent" "dev" { + arch = local.selected.coder_arch # amd64 or arm64 + os = "linux" +} + +data "aws_ami" "workspace" { + most_recent = true + owners = ["amazon"] + + filter { + name = "architecture" + values = [local.selected.arch] # x86_64 or arm64 + } +} +``` + +### Use the catalog without a parameter + +Set `create_parameter = false` to skip the picker (for example when the template pins the size) while still using the `instances` catalog: + +```tf +module "aws_ec2_instance_type" { + source = "registry.coder.com/coder/aws-ec2-instance-type/coder" + version = "1.0.0" + create_parameter = false + default = "t4g.large" +} +``` + +## Related templates + +For a complete AWS EC2 template, see the following examples in the [Coder Registry](https://registry.coder.com/). + +- [AWS EC2 (Linux)](https://registry.coder.com/templates/aws-linux) +- [AWS EC2 (Windows)](https://registry.coder.com/templates/aws-windows) diff --git a/registry/coder/modules/aws-ec2-instance-type/instance-types.json b/registry/coder/modules/aws-ec2-instance-type/instance-types.json new file mode 100644 index 000000000..0665809e5 --- /dev/null +++ b/registry/coder/modules/aws-ec2-instance-type/instance-types.json @@ -0,0 +1,408 @@ +[ + { + "type": "t3.nano", + "vcpus": 2, + "memory_mib": 512, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.nano", + "vcpus": 2, + "memory_mib": 512, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "t3.micro", + "vcpus": 2, + "memory_mib": 1024, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.micro", + "vcpus": 2, + "memory_mib": 1024, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "t3.small", + "vcpus": 2, + "memory_mib": 2048, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.small", + "vcpus": 2, + "memory_mib": 2048, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "c5.large", + "vcpus": 2, + "memory_mib": 4096, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.medium", + "vcpus": 1, + "memory_mib": 4096, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "t3.medium", + "vcpus": 2, + "memory_mib": 4096, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.medium", + "vcpus": 2, + "memory_mib": 4096, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "c5.xlarge", + "vcpus": 4, + "memory_mib": 8192, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m5.large", + "vcpus": 2, + "memory_mib": 8192, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.large", + "vcpus": 2, + "memory_mib": 8192, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "t3.large", + "vcpus": 2, + "memory_mib": 8192, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.large", + "vcpus": 2, + "memory_mib": 8192, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "i3.large", + "vcpus": 2, + "memory_mib": 15616, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.2xlarge", + "vcpus": 8, + "memory_mib": 16384, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.xlarge", + "vcpus": 4, + "memory_mib": 16384, + "gpus": 1, + "arch": "x86_64" + }, + { + "type": "m5.xlarge", + "vcpus": 4, + "memory_mib": 16384, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.xlarge", + "vcpus": 4, + "memory_mib": 16384, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "r5.large", + "vcpus": 2, + "memory_mib": 16384, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t3.xlarge", + "vcpus": 4, + "memory_mib": 16384, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.xlarge", + "vcpus": 4, + "memory_mib": 16384, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "i3.xlarge", + "vcpus": 4, + "memory_mib": 31232, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.4xlarge", + "vcpus": 16, + "memory_mib": 32768, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.2xlarge", + "vcpus": 8, + "memory_mib": 32768, + "gpus": 1, + "arch": "x86_64" + }, + { + "type": "m5.2xlarge", + "vcpus": 8, + "memory_mib": 32768, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.2xlarge", + "vcpus": 8, + "memory_mib": 32768, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "r5.xlarge", + "vcpus": 4, + "memory_mib": 32768, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t3.2xlarge", + "vcpus": 8, + "memory_mib": 32768, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "t4g.2xlarge", + "vcpus": 8, + "memory_mib": 32768, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "i3.2xlarge", + "vcpus": 8, + "memory_mib": 62464, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.4xlarge", + "vcpus": 16, + "memory_mib": 65536, + "gpus": 1, + "arch": "x86_64" + }, + { + "type": "m5.4xlarge", + "vcpus": 16, + "memory_mib": 65536, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.4xlarge", + "vcpus": 16, + "memory_mib": 65536, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "r5.2xlarge", + "vcpus": 8, + "memory_mib": 65536, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.9xlarge", + "vcpus": 36, + "memory_mib": 73728, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.12xlarge", + "vcpus": 48, + "memory_mib": 98304, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "i3.4xlarge", + "vcpus": 16, + "memory_mib": 124928, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.8xlarge", + "vcpus": 32, + "memory_mib": 131072, + "gpus": 1, + "arch": "x86_64" + }, + { + "type": "m5.8xlarge", + "vcpus": 32, + "memory_mib": 131072, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.8xlarge", + "vcpus": 32, + "memory_mib": 131072, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "r5.4xlarge", + "vcpus": 16, + "memory_mib": 131072, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.18xlarge", + "vcpus": 72, + "memory_mib": 147456, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "c5.24xlarge", + "vcpus": 96, + "memory_mib": 196608, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.12xlarge", + "vcpus": 48, + "memory_mib": 196608, + "gpus": 4, + "arch": "x86_64" + }, + { + "type": "m5.12xlarge", + "vcpus": 48, + "memory_mib": 196608, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.12xlarge", + "vcpus": 48, + "memory_mib": 196608, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "i3.8xlarge", + "vcpus": 32, + "memory_mib": 249856, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "g4dn.16xlarge", + "vcpus": 64, + "memory_mib": 262144, + "gpus": 1, + "arch": "x86_64" + }, + { + "type": "m5.16xlarge", + "vcpus": 64, + "memory_mib": 262144, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m7g.16xlarge", + "vcpus": 64, + "memory_mib": 262144, + "gpus": 0, + "arch": "arm64" + }, + { + "type": "r5.8xlarge", + "vcpus": 32, + "memory_mib": 262144, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "m5.24xlarge", + "vcpus": 96, + "memory_mib": 393216, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "r5.12xlarge", + "vcpus": 48, + "memory_mib": 393216, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "i3.16xlarge", + "vcpus": 64, + "memory_mib": 499712, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "r5.16xlarge", + "vcpus": 64, + "memory_mib": 524288, + "gpus": 0, + "arch": "x86_64" + }, + { + "type": "r5.24xlarge", + "vcpus": 96, + "memory_mib": 786432, + "gpus": 0, + "arch": "x86_64" + } +] diff --git a/registry/coder/modules/aws-ec2-instance-type/main.tf b/registry/coder/modules/aws-ec2-instance-type/main.tf new file mode 100644 index 000000000..5debffd82 --- /dev/null +++ b/registry/coder/modules/aws-ec2-instance-type/main.tf @@ -0,0 +1,151 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +variable "display_name" { + description = "The display name of the parameter." + type = string + default = "AWS EC2 Instance Type" +} + +variable "description" { + description = "The description of the parameter." + type = string + default = "Select the EC2 instance type to use for the workspace. See https://aws.amazon.com/ec2/instance-types/ for details." +} + +variable "default" { + description = "The default instance type to preselect (must belong to an included family), or the fixed value returned when create_parameter is false." + type = string + default = "" +} + +variable "create_parameter" { + description = "Whether to create the built-in coder_parameter. Set to false to skip the picker and return default while still exposing the instances catalog." + type = bool + default = true +} + +variable "mutable" { + description = "Whether the parameter can be changed after creation." + type = bool + default = false +} + +variable "custom_metadata" { + description = "Per instance type overrides keyed by instance type ID. name replaces the generated \"vCPU / RAM / arch\" label; description replaces the instance-type tooltip." + type = map(object({ + name = optional(string) + description = optional(string) + })) + default = {} +} + +variable "include" { + description = "Instance families to offer in the picker, e.g. [\"t3\", \"m5\", \"c5\"]. Defaults to t3." + type = list(string) + default = ["t3"] + + validation { + condition = length(var.include) > 0 + error_message = "include must list at least one instance family, e.g. [\"t3\"]." + } +} + +variable "coder_parameter_order" { + 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)." + type = number + default = null +} + +locals { + # Specs come straight from `aws ec2 describe-instance-types` (regenerate with + # .scripts/update.sh). coder_arch is derived from arch here because AWS spells + # it x86_64/arm64 while Coder uses amd64/arm64. + # + # Read instance-types.json with a fallback: Terraform resolves file() from the + # root module, but Coder's dynamic parameters preview resolves it from this + # module's directory, so neither path works on its own. + raw_instances = jsondecode(try(file("${path.module}/instance-types.json"), file("instance-types.json"))) + + instance_types = [ + for instance in local.raw_instances : merge(instance, { + coder_arch = instance.arch == "arm64" ? "arm64" : "amd64" + }) + ] + + included_instances = [ + for instance in local.instance_types : instance + if contains(var.include, split(".", instance.type)[0]) + ] + + spec_labels = { + for instance in local.included_instances : instance.type => format( + "%d vCPU, %g GiB RAM%s (%s)", + instance.vcpus, + instance.memory_mib / 1024, + instance.gpus > 0 ? format(", %d GPU", instance.gpus) : "", + instance.coder_arch + ) + } + + label_counts = { + for label in distinct(values(local.spec_labels)) : label => + length([for l in values(local.spec_labels) : l if l == label]) + } + + # spec label, with the instance type appended when families share a label so + # option names stay unique (Coder requires that). + display_labels = { + for instance_type, spec_label in local.spec_labels : instance_type => + local.label_counts[spec_label] > 1 ? "${trimsuffix(spec_label, ")")}, ${instance_type})" : spec_label + } + + # Final picker entries in the catalog's memory-sorted order. name is the spec + # label unless custom_metadata overrides it; description is the instance type, + # shown as the option's tooltip. + options = [ + for instance in local.included_instances : { + instance_type = instance.type + name = coalesce(try(var.custom_metadata[instance.type].name, null), local.display_labels[instance.type]) + description = coalesce(try(var.custom_metadata[instance.type].description, null), instance.type) + } + ] +} + +data "coder_parameter" "instance_type" { + count = var.create_parameter ? 1 : 0 + name = "aws_ec2_instance_type" + display_name = var.display_name + description = var.description + default = var.default == "" ? null : var.default + order = var.coder_parameter_order + mutable = var.mutable + dynamic "option" { + # local.options maps each instance type to its picker name and description. + for_each = local.options + iterator = opt + content { + value = opt.value.instance_type + name = opt.value.name + description = opt.value.description + } + } +} + +output "value" { + description = "The selected AWS EC2 instance type. Use it as the key into the instances output." + value = var.create_parameter ? one(data.coder_parameter.instance_type[*].value) : var.default +} + +output "instances" { + description = "All AWS EC2 instance types keyed by instance type ID, with raw specs (vcpus, memory_mib, gpus) and architecture (coder_arch, arch)." + value = { for instance in local.instance_types : instance.type => instance } +} diff --git a/registry/coder/modules/aws-ec2-instance-type/main.tftest.hcl b/registry/coder/modules/aws-ec2-instance-type/main.tftest.hcl new file mode 100644 index 000000000..01ca374a7 --- /dev/null +++ b/registry/coder/modules/aws-ec2-instance-type/main.tftest.hcl @@ -0,0 +1,190 @@ +run "parameter_name" { + command = plan + + assert { + condition = data.coder_parameter.instance_type[0].name == "aws_ec2_instance_type" + error_message = "Parameter name should be aws_ec2_instance_type" + } +} + +run "custom_order" { + command = plan + + variables { + coder_parameter_order = 99 + } + + assert { + condition = data.coder_parameter.instance_type[0].order == 99 + error_message = "coder_parameter_order should propagate to the parameter order" + } +} + +run "default_output" { + command = apply + + assert { + condition = output.value == "" + error_message = "value should be empty when no default is set" + } +} + +run "custom_default" { + command = apply + + variables { + default = "t3.large" + } + + assert { + condition = output.value == "t3.large" + error_message = "value should match the configured default" + } +} + +run "disabling_parameter_skips_creation" { + command = apply + + variables { + create_parameter = false + default = "t4g.large" + } + + assert { + condition = length(data.coder_parameter.instance_type) == 0 + error_message = "No coder_parameter should be created when create_parameter is false" + } + + assert { + condition = output.value == "t4g.large" + error_message = "value should fall back to default when the parameter is disabled" + } +} + +run "default_include_covers_template_types" { + command = apply + + assert { + condition = alltrue([ + for v in ["t3.micro", "t3.small", "t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge"] : + contains([for o in data.coder_parameter.instance_type[0].option : o.value], v) + ]) + error_message = "The default t3 family must include every instance type used by the AWS templates" + } +} + +run "include_filters_by_family" { + command = apply + + variables { + include = ["c5"] + } + + assert { + condition = contains([for o in data.coder_parameter.instance_type[0].option : o.value], "c5.large") && !contains([for o in data.coder_parameter.instance_type[0].option : o.value], "t3.micro") + error_message = "include should show the requested family and hide others" + } +} + +run "include_allows_multiple_families" { + command = apply + + variables { + include = ["t3", "c5"] + } + + assert { + condition = contains([for o in data.coder_parameter.instance_type[0].option : o.value], "t3.micro") && contains([for o in data.coder_parameter.instance_type[0].option : o.value], "c5.large") && !contains([for o in data.coder_parameter.instance_type[0].option : o.value], "m5.large") + error_message = "include should accept multiple families and exclude the rest" + } + + assert { + condition = length([for o in data.coder_parameter.instance_type[0].option : o if o.value == "c5.large" && o.name == "2 vCPU, 4 GiB RAM (amd64, c5.large)"]) == 1 + error_message = "a spec label shared across families should be disambiguated with the instance type" + } + + assert { + condition = length([for o in data.coder_parameter.instance_type[0].option : o if o.value == "t3.large" && o.name == "2 vCPU, 8 GiB RAM (amd64)"]) == 1 + error_message = "a unique spec label should stay specs-only" + } +} + +run "empty_include_is_rejected" { + command = plan + + variables { + include = [] + } + + expect_failures = [var.include] +} + +run "option_name_is_specs_and_tooltip_is_instance_type" { + command = apply + + assert { + condition = length([for o in data.coder_parameter.instance_type[0].option : o if o.value == "t3.medium" && o.name == "2 vCPU, 4 GiB RAM (amd64)" && o.description == "t3.medium"]) == 1 + error_message = "Option name should be the computed specs with arch and the description should be the instance type" + } +} + +run "custom_metadata_overrides" { + command = apply + + variables { + custom_metadata = { + "t3.medium" = { + name = "Standard" + description = "custom" + } + } + } + + assert { + condition = length([for o in data.coder_parameter.instance_type[0].option : o if o.value == "t3.medium" && o.name == "Standard" && o.description == "custom"]) == 1 + error_message = "custom_metadata should override the option name and description" + } +} + +run "instances_output_exposes_specs_and_derived_fields" { + command = plan + + assert { + condition = length(output.instances) == length(local.instance_types) + error_message = "The instances output should expose every catalog entry keyed by value" + } + + assert { + condition = output.instances["t3.medium"].vcpus == 2 && output.instances["t3.medium"].memory_mib == 4096 && output.instances["t3.medium"].gpus == 0 + error_message = "Raw specs should be exposed as numeric fields" + } + + assert { + condition = output.instances["t3.medium"].coder_arch == "amd64" && output.instances["t3.medium"].arch == "x86_64" + error_message = "x86 instances should derive amd64 from the x86_64 arch" + } + + assert { + condition = output.instances["m7g.large"].coder_arch == "arm64" && output.instances["m7g.large"].arch == "arm64" + error_message = "Graviton instances should derive arm64" + } + + assert { + condition = output.instances["g4dn.12xlarge"].gpus == 4 + error_message = "gpus should reflect the source data" + } +} + +run "value_is_key_in_instances" { + command = apply + + variables { + include = ["t4g"] + default = "t4g.medium" + } + + assert { + condition = contains(keys(output.instances), output.value) && output.instances[output.value].coder_arch == "arm64" + error_message = "The selected value must be a key in the instances output" + } +}