Skip to content
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

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

Open
1 task done
bonnnk opened this issue Nov 8, 2022 · 1 comment
Labels
bug service/route upstream/terraform This issue is blocked on an upstream issue within Terraform (Terraform Core/CLI, The Plugin SDK etc) v/3.x

Comments

@bonnnk
Copy link

bonnnk commented Nov 8, 2022

Is there an existing issue for this?

  • I have searched the existing issues

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

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 bonnnk added the bug label Nov 8, 2022
@github-actions github-actions bot removed the bug label Nov 8, 2022
@bonnnk
Copy link
Author

bonnnk commented Nov 8, 2022

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:

  • "address_prefix" is required
  • "security_group" is optional

For azurerm_subnet:

  • "address_prefixes" is required ("address_prefix" isn't an option)
  • "security_group" isn't an option

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"]
      }
    }
  }
}

@manicminer manicminer added bug upstream/terraform This issue is blocked on an upstream issue within Terraform (Terraform Core/CLI, The Plugin SDK etc) service/route labels Nov 14, 2022
@rcskosir rcskosir added the v/3.x label Oct 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug service/route upstream/terraform This issue is blocked on an upstream issue within Terraform (Terraform Core/CLI, The Plugin SDK etc) v/3.x
Projects
None yet
Development

No branches or pull requests

3 participants