spotinst / terraform-provider-spotinst

Terraform Spotinst provider.
https://registry.terraform.io/providers/spotinst/spotinst/latest/docs
Mozilla Public License 2.0
63 stars 56 forks source link

Resource `spotinst_ocean_aws_launch_spec` using `instance_types` attribute with empty `dynamic "instance_types_filters"` nested block fails to apply due to API errors #561

Closed snooyen closed 5 months ago

snooyen commented 5 months ago

Terraform Version

Terraform v1.5.7
spotinst/spotinst v1.180.1

Affected Resource(s)

Terraform Configuration Files

# eks/spotinst-ocean-vng.tf

variable "spotinst_ocean_vngs" {
  type = map(object({
. . .
    instance_types = optional(list(string), null)
    instance_types_filters = optional(
      object({
        categories              = optional(list(string), null)
        disk_types              = optional(list(string), null)
        exclude_families        = optional(list(string), null)
        exclude_metal           = optional(bool, false)
        hypervisor              = optional(list(string), null)
        include_families        = optional(list(string), null)
        is_ena_supported        = optional(bool, null)
        max_gpu                 = optional(number, null)
        max_memory_gib          = optional(number, null)
        max_network_performance = optional(number, null)
        max_vcpu                = optional(number, null)
        min_enis                = optional(number, null)
        min_gpu                 = optional(number, null)
        min_memory_gib          = optional(number, null)
        min_network_performance = optional(number, null)
        min_vcpu                = optional(number, null)
        root_device_types       = optional(list(string), null)
        virtualization_types    = optional(list(string), null)
    }), {})
. . .
  }))
  description = "List of objects defining a Spotinst Ocean Virtual Node Group for the cluster"
  default     = {}
}

resource "spotinst_ocean_aws_launch_spec" "this" {
    for_each = local.enabled ? var.spotinst_ocean_vngs : {}
. . .
    instance_types       = each.value.instance_types

    dynamic "instance_types_filters" {
      for_each = each.value.instance_types_filters != {} ? [each.value.instance_types_filters] : []

      content {
            categories              = instance_types_filters.value.categories
            disk_types              = instance_types_filters.value.disk_types
            exclude_families        = instance_types_filters.value.exclude_families
            exclude_metal           = instance_types_filters.value.exclude_metal
            hypervisor              = instance_types_filters.value.hypervisor
            include_families        = instance_types_filters.value.include_families
            is_ena_supported        = instance_types_filters.value.is_ena_supported
            max_gpu                 = instance_types_filters.value.max_gpu
            max_memory_gib          = instance_types_filters.value.max_memory_gib
            max_network_performance = instance_types_filters.value.max_network_performance
            max_vcpu                = instance_types_filters.value.max_vcpu
            min_enis                = instance_types_filters.value.min_enis
            min_gpu                 = instance_types_filters.value.min_gpu
            min_memory_gib          = instance_types_filters.value.min_memory_gib
            min_network_performance = instance_types_filters.value.min_network_performance
            min_vcpu                = instance_types_filters.value.min_vcpu
            root_device_types       = instance_types_filters.value.root_device_types
            virtualization_types    = instance_types_filters.value.virtualization_types
      }
  }
. . . 
}  
# eks/myvars.tfvars
spotinst_ocean_vngs = {
"my-vng" = {
    instance_types = [ 'p2.xlarge',  'p3.2xlarge']
   #  instance_types_filters = {}
  }
}

Debug Output

Here's an excerpt where we were trying to update the AMI for a specific VNG that was using instance_types attribute:

2024-07-02T23:47:32.762Z [DEBUG] spotinst_ocean_aws_launch_spec.this["my-vng"]: applying the planned Update change
2024-07-02T23:47:32.773Z [INFO]  provider.terraform-provider-spotinst_v1.180.1: onUpdate() -> spotinst_ocean_aws_launch_spec -> started for ols-XXXXXXXX...: timestamp=2024-07-02T23:47:32.772Z
2024-07-02T23:47:32.774Z [INFO]  provider.terraform-provider-spotinst_v1.180.1: onUpdate() -> Ocean_AWS_Launch_Spec -> image_id: timestamp=2024-07-02T23:47:32.773Z
2024-07-02T23:47:32.774Z [INFO]  provider.terraform-provider-spotinst_v1.180.1: onUpdate() -> Ocean_AWS_Instance_Types -> instance_types_filters: timestamp=2024-07-02T23:47:32.773Z
2024-07-02T23:47:32.777Z [INFO]  provider.terraform-provider-spotinst_v1.180.1: ===> launchSpec update configuration: {
  "autoScale": {},
  "id": "ols-XXXXXXXX",
  "imageId": "ami-XXXXXXXXXXXXXXXXX",
  "instanceTypesFilters": {
    "categories": null,
    "diskTypes": null,
    "excludeFamilies": null,
    "excludeMetal": false,
    "hypervisor": null,
    "includeFamilies": null,
    "isEnaSupported": null,
    "maxGpu": null,
    "maxMemoryGiB": null,
    "maxNetworkPerformance": null,
    "maxVcpu": null,
    "minEnis": null,
    "minGpu": null,
    "minMemoryGiB": null,
    "minNetworkPerformance": null,
    "minVcpu": null,
    "rootDeviceTypes": null,
    "virtualizationTypes": null
  },
  "scheduling": {}
}: timestamp=2024-07-02T23:47:32.776Z
2024-07-02T23:47:32.777Z [DEBUG] provider.terraform-provider-spotinst_v1.180.1: [spotinst-sdk-go] SPOTINST: Request "PUT https://api.spotinst.io/ocean/aws/k8s/launchSpec/ols-XXXXXXXX?accountId=act-XXXXXXXX" details:
---[ REQUEST ]---------------------------------------
PUT /ocean/aws/k8s/launchSpec/ols-XXXXXXXX?accountId=act-XXXXXXXX HTTP/1.1
Host: api.spotinst.io
User-Agent: HashiCorp/1.0 Terraform/1.5.7 (+https://www.terraform.io) Terraform Plugin SDK/2.5.0 Terraform Provider Spotinst/v2-1.180.1 spotinst-sdk-go/1.358.0 (go1.20.14; linux; amd64)
Content-Length: 470
Accept: application/json
Authorization: Bearer <REDACTED>
Content-Type: application/json
Accept-Encoding: gzip
{
    "launchSpec": {
        "autoScale": {},
        "imageId": "ami-XXXXXXXXXXXXXXXXX",
        "instanceTypesFilters": {
            "categories": null,
            "diskTypes": null,
            "excludeFamilies": null,
            "excludeMetal": false,
            "hypervisor": null,
            "includeFamilies": null,
            "isEnaSupported": null,
            "maxGpu": null,
            "maxMemoryGiB": null,
            "maxNetworkPerformance": null,
            "maxVcpu": null,
            "minEnis": null,
            "minGpu": null,
            "minMemoryGiB": null,
            "minNetworkPerformance": null,
            "minVcpu": null,
            "rootDeviceTypes": null,
            "virtualizationTypes": null
        },
        "scheduling": {}
    }
}

Expected Behavior

Actual Behavior

Steps to Reproduce

  1. terraform apply

Important Factoids

Community Note

snooyen commented 5 months ago

Upon further inspection, we realized that the issue lies in our specified default values. Specifically:

variable "spotinst_ocean_vngs" {
  type = map(object({
. . .
    instance_types = optional(list(string), null)
    instance_types_filters = optional(
. . .
      exclude_metal           = optional(bool, false)
. . .
default     = {}

resulting in an incorrect evaluation of the for_each statement inside the dynamic instance_types_filters nested block.

Changing things to the following resulted in expected behavior:

# eks/spotinst-ocean-vng.tf

variable "spotinst_ocean_vngs" {
  type = map(object({
. . .
    instance_types = optional(list(string), null)
    instance_types_filters = optional(
      object({
        categories              = optional(list(string), null)
        disk_types              = optional(list(string), null)
        exclude_families        = optional(list(string), null)
        exclude_metal           = optional(bool, null)
        hypervisor              = optional(list(string), null)
        include_families        = optional(list(string), null)
        is_ena_supported        = optional(bool, null)
        max_gpu                 = optional(number, null)
        max_memory_gib          = optional(number, null)
        max_network_performance = optional(number, null)
        max_vcpu                = optional(number, null)
        min_enis                = optional(number, null)
        min_gpu                 = optional(number, null)
        min_memory_gib          = optional(number, null)
        min_network_performance = optional(number, null)
        min_vcpu                = optional(number, null)
        root_device_types       = optional(list(string), null)
        virtualization_types    = optional(list(string), null)
    }), {})
. . .
  }))
  description = "List of objects defining a Spotinst Ocean Virtual Node Group for the cluster"
  default     = null
}

resource "spotinst_ocean_aws_launch_spec" "this" {
    for_each = local.enabled ? var.spotinst_ocean_vngs : {}
. . .
    instance_types       = each.value.instance_types

    dynamic "instance_types_filters" {
      for_each = each.value.instance_types_filters != null ? [each.value.instance_types_filters] : []

      content {
            categories              = instance_types_filters.value.categories
            disk_types              = instance_types_filters.value.disk_types
            exclude_families        = instance_types_filters.value.exclude_families
            exclude_metal           = instance_types_filters.value.exclude_metal
            hypervisor              = instance_types_filters.value.hypervisor
            include_families        = instance_types_filters.value.include_families
            is_ena_supported        = instance_types_filters.value.is_ena_supported
            max_gpu                 = instance_types_filters.value.max_gpu
            max_memory_gib          = instance_types_filters.value.max_memory_gib
            max_network_performance = instance_types_filters.value.max_network_performance
            max_vcpu                = instance_types_filters.value.max_vcpu
            min_enis                = instance_types_filters.value.min_enis
            min_gpu                 = instance_types_filters.value.min_gpu
            min_memory_gib          = instance_types_filters.value.min_memory_gib
            min_network_performance = instance_types_filters.value.min_network_performance
            min_vcpu                = instance_types_filters.value.min_vcpu
            root_device_types       = instance_types_filters.value.root_device_types
            virtualization_types    = instance_types_filters.value.virtualization_types
      }
  }
. . . 
}