hashicorp / terraform-provider-azurerm

Terraform provider for Azure Resource Manager
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
Mozilla Public License 2.0
4.59k stars 4.63k forks source link

"next_hop_in_ip_address" interpreting null as empty string when used in for_each loop with flatten function #19182

Open bonnnk opened 1 year ago

bonnnk commented 1 year ago

Is there an existing issue for this?

Community Note

Terraform Version

Seen in 1.2.6 and 1.3.4

AzureRM Provider Version

Seen in 3.18 and 3.30.0

Affected Resource(s)/Data Source(s)

azurerm_route

Terraform Configuration Files

Module layout:

.
├── eastUS.tfvars
├── main.tf
├── modules
│   ├── network
│   │   ├── subnet
│   │   │   ├── route_table.tf
│   │   │   └── variables.tf
└── variables.tf

Child Module:

variables.tf


variable "location" {
  type        = string
  description = "Defines the network interfaces location."
}

variable "resource_group_name" {
  description = "The address prefix to use for the subnet."
}

variable "route_tables" {
  type = map(object({
    route_table_name                          = string
    route_table_disable_bgp_route_propagation = bool
    route_entries = map(object({
      route_entry_name                   = string
      route_entry_address_prefix         = string
      route_entry_next_hop_type          = string
      route_entry_next_hop_in_ip_address = string
    }))
  }))
}

route_table.tf

locals {
  all_route_entries = flatten([
    for route_table_key, route_table_value in var.route_tables : [
      for route_entry_key, route_entry_value in route_table_value.route_entries : {
        route_table_key                      = route_table_key
        route_table_name                     = route_table_value["route_table_name"]
        route_entry_key                      = route_entry_key
        route_entry_name                     = route_entry_value["route_entry_name"]
        route_entry_address_prefix           = route_entry_value["route_entry_address_prefix"]
        route_entry_next_hop_type            = route_entry_value["route_entry_next_hop_type"]
        route_entry_next_hop_in_ip_address   = route_entry_value["route_entry_next_hop_in_ip_address"]
      }
    ]
  ])
}

resource "azurerm_route_table" "route_table" {
  for_each                      = var.route_tables

  resource_group_name           = var.resource_group_name
  location                      = var.location
  name                          = each.value.route_table_name
  disable_bgp_route_propagation = each.value.route_table_disable_bgp_route_propagation

  route = [ for key, value in each.value.route_entries : azurerm_route.route_entry[key] ]
}

resource "azurerm_route" "route_entry" {
  for_each = {
    for route_entry in local.all_route_entries : route_entry.route_entry_key => route_entry
  }

  resource_group_name    = var.resource_group_name
  name                   = each.value.route_entry_name
  route_table_name       = each.value.route_table_name
  address_prefix         = each.value.route_entry_address_prefix
  next_hop_type          = each.value.route_entry_next_hop_type
  next_hop_in_ip_address = each.value.route_entry_next_hop_in_ip_address
}

Parent Module:

variables.tf

variable "resource_group_name" {
  type        = string
  description = "Defines the network interface's resource group name."
}

variable "location" {
  type        = string
  description = "Defines the network interfaces location."
}

variable "route_tables" {
  type = map(object({
    route_table_name                          = string
    route_table_disable_bgp_route_propagation = bool
    route_entries = map(object({
      route_entry_name                   = string
      route_entry_address_prefix         = string
      route_entry_next_hop_type          = string
      route_entry_next_hop_in_ip_address = string
    }))
  }))
}

main.tf

module "subnet" {
  source                                    = "./modules/network/subnet"
  resource_group_name                       = data.azurerm_resource_group.example.name
  location                                  = data.azurerm_resource_group.example.location
  route_tables                              = var.route_tables
}

eastUS.tfvars

route_tables = {
  my-route-table-name = {
    route_table_name                          = "my-route-table-name"
    route_table_disable_bgp_route_propagation = false
    route_entries = {
      test_route_entry = {
        route_entry_name                   = "test_route_entry"
        route_entry_address_prefix         = "0.0.0.0/0"
        route_entry_next_hop_type          = "None"
        route_entry_next_hop_in_ip_address = null
      }
    }
  }
}

Debug Output/Panic Output

2022-11-08T09:29:24.909-0600 [DEBUG] provider.terraform-provider-azurerm_v3.30.0_x5: AzureRM Response for https://management.azure.com/subscriptions/my-subscription/resourceGroups/example/providers/Microsoft.Network/routeTables/my-route-table-name/routes/test_route_entry?api-version=2021-08-01: 
HTTP/2.0 200 OK
Cache-Control: no-cache
Content-Type: application/json; charset=utf-8
Date: Tue, 08 Nov 2022 15:29:24 GMT
Etag: W/"8310c801-0e05-4f6e-b222-77d294e8ac84"
Expires: -1
Pragma: no-cache
Server: Microsoft-HTTPAPI/2.0
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Ms-Arm-Service-Request-Id: 9dec5976-615a-4d0d-9d12-51d87863fcd1
X-Ms-Correlation-Request-Id: 25db59b5-4253-9744-d58f-f2fb96ea5651
X-Ms-Ratelimit-Remaining-Subscription-Reads: 11997
X-Ms-Request-Id: ee62c7aa-6fbf-4829-ae28-80446dfa2700
X-Ms-Routing-Request-Id: SOUTHCENTRALUS:20221108T152924Z:06ce8917-6fe8-44c5-9137-218c788853a0

{
  "name": "test_route_entry",
  "id": "/subscriptions/my-subscription/resourceGroups/example/providers/Microsoft.Network/routeTables/my-route-table-name/routes/test_route_entry",
  "etag": "W/\"8310c801-0e05-4f6e-b222-77d294e8ac84\"",
  "properties": {
    "provisioningState": "Succeeded",
    "addressPrefix": "0.0.0.0/0",
    "nextHopType": "None",
    "hasBgpOverride": false
  },
  "type": "Microsoft.Network/routeTables/routes"
}: timestamp=2022-11-08T09:29:24.909-0600
2022-11-08T09:29:24.909-0600 [DEBUG] provider.terraform-provider-azurerm_v3.30.0_x5: Unlocking "azurerm_route_table.my-route-table-name": timestamp=2022-11-08T09:29:24.909-0600
2022-11-08T09:29:24.909-0600 [DEBUG] provider.terraform-provider-azurerm_v3.30.0_x5: Unlocked "azurerm_route_table.my-route-table-name": timestamp=2022-11-08T09:29:24.909-0600
2022-11-08T09:29:24.910-0600 [WARN]  Provider "provider[\"registry.terraform.io/hashicorp/azurerm\"]" produced an unexpected new value for module.subnet.azurerm_route.route_entry["test_route_entry"], but we are tolerating it because it is using the legacy plugin SDK.
    The following problems may be the cause of any confusing errors from downstream operations:
      - .next_hop_in_ip_address: was null, but now cty.StringVal("")
2022-11-08T09:29:24.936-0600 [WARN]  provider.terraform-provider-azurerm_v3.30.0_x5: Truncating attribute path of 1 diagnostics for TypeSet: timestamp=2022-11-08T09:29:24.936-0600
2022-11-08T09:29:24.936-0600 [ERROR] provider.terraform-provider-azurerm_v3.30.0_x5: Response contains error diagnostic: tf_provider_addr=provider tf_req_id=ddd794a9-b3c1-154a-c1b5-faed0f1a9aec tf_resource_type=azurerm_route_table @caller=github.com/hashicorp/terraform-plugin-go@v0.10.0/tfprotov5/internal/diag/diagnostics.go:56 @module=sdk.proto diagnostic_detail= diagnostic_summary="expected "route.0.next_hop_in_ip_address" to not be an empty string, got " tf_rpc=ValidateResourceTypeConfig diagnostic_attribute=AttributeName("route") diagnostic_severity=ERROR tf_proto_version=5.2 timestamp=2022-11-08T09:29:24.936-0600
2022-11-08T09:29:24.937-0600 [ERROR] vertex "module.subnet.azurerm_route_table.route_table[\"my-route-table-name\"]" error: expected "route.0.next_hop_in_ip_address" to not be an empty string, got
2022-11-08T09:29:24.960-0600 [DEBUG] provider.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = error reading from server: EOF"
2022-11-08T09:29:24.966-0600 [DEBUG] provider: plugin process exited: path=.terraform/providers/registry.terraform.io/hashicorp/azurerm/3.30.0/darwin_amd64/terraform-provider-azurerm_v3.30.0_x5 pid=93925
2022-11-08T09:29:24.966-0600 [DEBUG] provider: plugin exited

Expected Behaviour

Route entry "test_route_entry" is created and added to route table "my-route-table-name".

Plan Output

Terraform will perform the following actions:

  # module.subnet.azurerm_route.route_entry["test_route_entry"] will be created
  + resource "azurerm_route" "route_entry" {
      + address_prefix      = "0.0.0.0/0"
      + id                  = (known after apply)
      + name                = "test_route_entry"
      + next_hop_type       = "None"
      + resource_group_name = "example"
      + route_table_name    = "my-route-table-name"
    }

  # module.subnet.azurerm_route_table.route_table["my-route-table-name"] will be updated in-place
  ~ resource "azurerm_route_table" "route_table" {
        id                            = "/subscriptions/.../my-route-table-name
        name                          = "my-route-table-name"
      ~ route                         = [
          + {
              + address_prefix         = "0.0.0.0/0"
              + name                   = "test_route_entry"
              + next_hop_in_ip_address = ""
              + next_hop_type          = "None"
            },
        ]
        tags                          = {}
        # (4 unchanged attributes hidden)
    }

Plan: 1 to add, 1 to change, 0 to destroy.

Actual Behaviour

│ Error: expected "route.0.next_hop_in_ip_address" to not be an empty string, got 
│ 
│   with module.subnet.azurerm_route_table.route_table["my-route-table-name"],
│   on modules/network/subnet/route_table.tf line 25, in resource "azurerm_route_table" "route_table":
│   25:   route = [ for key, value in each.value.route_entries : azurerm_route.route_entry[key] ]

Steps to Reproduce

terraform plan -var-file="eastUS.tfvars" -out="eastUS" -lock=false
terraform apply "eastUS"

Important Factoids

"next_hop_in_ip_address" correctly interprets "null" when azurerm_route is outside of a flattened for_each loop.

resource "azurerm_route" "route_test" {
  resource_group_name = var.resource_group_name
  name                = "test_route_entry"
  route_table_name    = azurerm_route_table.route_table["my-test-route-table"].name
  address_prefix      = "0.0.0.0/0"
  next_hop_type       = "None"
  next_hop_in_ip_address = null
}

References

No response

bonnnk commented 1 year ago

I'm encountering the same problem with azurerm_virtual_network and azurerm_subnet resources. It seems the flatten function causes a "container" resource to require all of a"tenant" resource's arguments.

│ Error: Incorrect attribute value type
│ 
│   on modules/network/subnet/vnet.tf line 24, in resource "azurerm_virtual_network" "vnet":
│   24:   subnet = [ for key, value in each.value.subnets : azurerm_subnet.subnet[key] ]
│     ├────────────────
│     │ azurerm_subnet.subnet is object with 1 attribute "gateway_subnet"
│     │ each.value.subnets is map of object with 1 element
│ 
│ Inappropriate value for attribute "subnet": element 0: attributes "address_prefix" and "security_group" are required.

Note the arguments listed in the error message: "address_prefix" and "security_group". I shouldn't be receiving these errors since I'm not calling azurerm_virtual_network embedded subnet functionality.

For azurerm_virtual_network embedded subnet blocks:

For azurerm_subnet:

Child Module

variable "vnets" {
  type = map(object({
    vnet_name          = string
    vnet_address_space = list(string)
    subnets = map(object({
      subnet_name             = string
      subnet_vnet_name        = string
      subnet_address_prefixes = list(string)
    }))
  }))
}

locals {
  all_subnets = flatten([
    for vnet_key, vnet_value in var.vnets : [
      for subnet_key, subnet_value in vnet_value.subnets : {
        vnet_key                = vnet_key
        vnet_name               = vnet_value["vnet_name"]
        subnet_key              = subnet_key
        subnet_name             = subnet_value["subnet_name"]
        subnet_address_prefixes = subnet_value["subnet_address_prefixes"]
      }
    ]
  ])
}

resource "azurerm_virtual_network" "vnet" {
  for_each                = var.vnets

  resource_group_name     = var.resource_group_name
  location                = var.location
  name                    = each.value.vnet_name
  address_space           = each.value.vnet_address_space

  subnet = [ for key, value in each.value.subnets : azurerm_subnet.subnet[key] ]
}

resource "azurerm_subnet" "subnet" {
  for_each = {
    for subnet in local.all_subnets : subnet.subnet_key => subnet
  }

  resource_group_name   = var.resource_group_name
  name                  = each.value.subnet_name
  virtual_network_name  = each.value.vnet_name
  address_prefixes      = each.value.subnet_address_prefixes
}

Parent Module

variable "vnets" {
  type = map(object({
    vnet_name          = string
    vnet_address_space = list(string)
    subnets = map(object({
      subnet_name             = string
      subnet_vnet_name        = string
      subnet_address_prefixes = list(string)
    }))
  }))
}

module "subnet" {
  source                                    = "./modules/network/subnet"
  resource_group_name                       = data.azurerm_resource_group.poc-networkautomation-centralus-rg.name
  location                                  = data.azurerm_resource_group.poc-networkautomation-centralus-rg.location
  vnets                                     = var.vnets
}

vnets = {
  my-test-vnet = {
    vnet_name                    = "my-test-vnet"
    vnet_address_space           = ["10.2.0.0/24", "10.3.0.0/24"]
    subnets = {
      gateway_subnet = {
        subnet_name                                        = "GatewaySubnet"
        subnet_vnet_name                                   = "my-test-vnet"
        subnet_address_prefixes                            = ["10.2.0.0/27"]
      }
    }
  }
}