The given value is not suitable for module

Hi. Sorry, yes I get same error as you. I previously only pasted the second error.

Error: Invalid value for input variable
│ 
│   on main.tf line 3, in module "the_hosts":
│    3:   systems = {
│    4:     host01 = {
│    5:       interfaces = {
│    6:         leafs = [{
│    7:           device  = "en0"
│    8:           ip      = "10.X.X.X"
│    9:           netmask = "/24"
│   10:           gateway = "10.X.X.X"
│   11:           },
│   12:           { device  = "en1"
│   13:             ip      = "10.X.X.X"
│   14:             netmask = "/24"
│   15:             gateway = "10.X.X.X"
│   16:         }],
│   17:         lom = { ip = "X.X.X.X" },
│   18:         management = {
│   19:           device  = "en3"
│   20:           ip      = "X.X.X.X"
│   21:           netmask = "/24"
│   22:           gateway = "X.X.X.X"
│   23:           mac     = "aa:bb:cc:dd:ee:ff"
│   24:         },
│   25:         stable = {
│   26:           device = "lo"
│   27:           ip     = "X.X.X.X"
│   28:         }
│   29:       },
│   30:       disks = ["sda", "sdb"]
│   31:     },
│   32:     host02 = {
│   33:       interfaces = {
│   34:         leafs = [{
│   35:           device  = "en0"
│   36:           ip      = "10.X.X.X"
│   37:           netmask = "/24"
│   38:           gateway = "10.X.X.X"
│   39:           },
│   40:           { device  = "en1"
│   41:             ip      = "10.X.X.X"
│   42:             netmask = "/24"
│   43:             gateway = "10.X.X.X"
│   44:         }],
│   45:         lom = { ip = "X.X.X.X" },
│   46:         management = {
│   47:           device  = "en3"
│   48:           ip      = "X.X.X.X"
│   49:           netmask = "/24"
│   50:           gateway = "X.X.X.X"
│   51:           mac     = "aa:bb:cc:dd:ee:ff"
│   52:         },
│   53:         stable = {
│   54:           device = "lo"
│   55:           ip     = "X.X.X.X"
│   56:         }
│   57:       },
│   58:       disks = ["sda", "sdb"]
│   59:     }
│   60:   }
│ 
│ The given value is not suitable for module.the_hosts.var.systems declared at
│ hosts/variables.tf:1,1-19: element "host01": attribute "interfaces": element "management":
│ attributes "leafs", "lom", "management", and "stable" are required.

Cool, thanks!

So the error message seems to be talking about var.systems["host01"].interfaces["stable"]; the fact that it’s saying element "stable" rather than attribute "stable" suggests that Terraform is understanding this as a map key rather than as an attribute.

I think again there’s a missing level in your data structure: interfaces is defined as being a map of objects, but you’ve assigned only a single object to it, and so Terraform is trying its best to understand all of the attributes of that object as being elements of the interfaces map.

This seems similar to my last answer: either the type constraint needs to specify a single object instead of a map of objects, or the value you pass needs to include a map of objects. I feel like we’re not connecting here so I must be misunderstanding something about what’s going on; is your intent for each host to have only one interface, or should each host have zero or more interfaces where each one has a unique key?

One theory I have is that Terraform’s ability to automatically convert an object into a map is making it hard for you to see what is a map and what is an object in your value, since they’re all using the object syntax. Perhaps it would make things clearer to explicitly convert to maps in all of the places where the type constraint calls for maps, so it’s easier to see in the expression which of the objects are being passed directly as objects and which ones are intended to convert as maps:

module "the_hosts" {
  source = "./hosts"
  systems = tomap({
    host01 = {
      interfaces = tomap({
        leafs = [
          {
            device  = "en0"
            ip      = "10.X.X.X"
            netmask = "/24"
            gateway = "10.X.X.X"
          },
          { device  = "en1"
            ip      = "10.X.X.X"
            netmask = "/24"
            gateway = "10.X.X.X"
          }
        ],
        lom = { ip = "X.X.X.X" },
        management = {
          device  = "en3"
          ip      = "X.X.X.X"
          netmask = "/24"
          gateway = "X.X.X.X"
          mac     = "aa:bb:cc:dd:ee:ff"
        },
        stable = {
          device = "lo"
          ip     = "X.X.X.X"
        }
      }),
      disks = tolist(["sda", "sdb"])
    },
    host02 = {
      interfaces = tomap({
        leafs = [
          {
            device  = "en0"
            ip      = "10.X.X.X"
            netmask = "/24"
            gateway = "10.X.X.X"
          },
          { device  = "en1"
            ip      = "10.X.X.X"
            netmask = "/24"
            gateway = "10.X.X.X"
          }
        ],
        lom = { ip = "X.X.X.X" },
        management = {
          device  = "en3"
          ip      = "X.X.X.X"
          netmask = "/24"
          gateway = "X.X.X.X"
          mac     = "aa:bb:cc:dd:ee:00"
        },
        stable = {
          device = "lo"
          ip     = "X.X.X.X"
        }
      }),
      disks = tolist(["sda", "sdb"])
    }
  })
}

In the above I’ve added explicit tomap(...) and tolist(...) conversions in all of the places where Terraform would’ve previously attempted automatic conversions in order to meet the type constraint. This gives Terraform a little extra information about what types I was intending to produce, which allows it to produce a different error message:

╷
│ Error: Invalid function argument
│ 
│   on unsuitable-value.tf line 5, in module "the_hosts":
│    5:       interfaces = tomap({
│    6:         leafs = [
│    7:           {
│    8:             device  = "en0"
│    9:             ip      = "10.X.X.X"
│   10:             netmask = "/24"
│   11:             gateway = "10.X.X.X"
│   12:           },
│   13:           { device  = "en1"
│   14:             ip      = "10.X.X.X"
│   15:             netmask = "/24"
│   16:             gateway = "10.X.X.X"
│   17:           }
│   18:         ],
│   19:         lom = { ip = "X.X.X.X" },
│   20:         management = {
│   21:           device  = "en3"
│   22:           ip      = "X.X.X.X"
│   23:           netmask = "/24"
│   24:           gateway = "X.X.X.X"
│   25:           mac     = "aa:bb:cc:dd:ee:ff"
│   26:         },
│   27:         stable = {
│   28:           device = "lo"
│   29:           ip     = "X.X.X.X"
│   30:         }
│   31:       }),
│     ├────────────────
│     │ while calling tomap(v)
│ 
│ Invalid value for "v" parameter: cannot convert object to map of any single
│ type.
╵
╷
│ Error: Invalid function argument
│ 
│   on unsuitable-value.tf line 35, in module "the_hosts":
│   35:       interfaces = tomap({
│   36:         leafs = [
│   37:           {
│   38:             device  = "en0"
│   39:             ip      = "10.X.X.X"
│   40:             netmask = "/24"
│   41:             gateway = "10.X.X.X"
│   42:           },
│   43:           { device  = "en1"
│   44:             ip      = "10.X.X.X"
│   45:             netmask = "/24"
│   46:             gateway = "10.X.X.X"
│   47:           }
│   48:         ],
│   49:         lom = { ip = "X.X.X.X" },
│   50:         management = {
│   51:           device  = "en3"
│   52:           ip      = "X.X.X.X"
│   53:           netmask = "/24"
│   54:           gateway = "X.X.X.X"
│   55:           mac     = "aa:bb:cc:dd:ee:00"
│   56:         },
│   57:         stable = {
│   58:           device = "lo"
│   59:           ip     = "X.X.X.X"
│   60:         }
│   61:       }),
│     ├────────────────
│     │ while calling tomap(v)
│ 
│ Invalid value for "v" parameter: cannot convert object to map of any single
│ type.
╵

Because I gave Terraform precise type information instead of relying on automatic type conversions, Terraform was able to be more specific about what’s wrong here: it identified this tomap call in particular as being incorrect, because the argument I passed it is an interface object rather than a map of interface objects. The text of this error message is admittedly far less specific than the one we had before, but it has the benefit of drawing attention to the specific part of the data structure that was wrong, and Terraform announcing what it was trying to do when it failed.

Making that actually be a map of objects made it validate:

module "the_hosts" {
  source = "./child"
  systems = tomap({
    host01 = {
      interfaces = tomap({
        a = {
          leafs = [
            {
              device  = "en0"
              ip      = "10.X.X.X"
              netmask = "/24"
              gateway = "10.X.X.X"
            },
            { device  = "en1"
              ip      = "10.X.X.X"
              netmask = "/24"
              gateway = "10.X.X.X"
            }
          ],
          lom = { ip = "X.X.X.X" },
          management = {
            device  = "en3"
            ip      = "X.X.X.X"
            netmask = "/24"
            gateway = "X.X.X.X"
            mac     = "aa:bb:cc:dd:ee:ff"
          },
          stable = {
            device = "lo"
            ip     = "X.X.X.X"
          }
        },
      }),
      disks = tolist(["sda", "sdb"])
    },
    host02 = {
      interfaces = tomap({
        a = {
          leafs = [
            {
              device  = "en0"
              ip      = "10.X.X.X"
              netmask = "/24"
              gateway = "10.X.X.X"
            },
            { device  = "en1"
              ip      = "10.X.X.X"
              netmask = "/24"
              gateway = "10.X.X.X"
            }
          ],
          lom = { ip = "X.X.X.X" },
          management = {
            device  = "en3"
            ip      = "X.X.X.X"
            netmask = "/24"
            gateway = "X.X.X.X"
            mac     = "aa:bb:cc:dd:ee:00"
          },
          stable = {
            device = "lo"
            ip     = "X.X.X.X"
          }
        },
      }),
      disks = tolist(["sda", "sdb"])
    }
  })
}

The interface for each host is in a map element whose key is a, again because I don’t really know enough about what you’re modelling to choose a more realistic key.

My intent is to model five network interfaces on each host.

Each with unique key.

leafs = a list of network interfaces, always 2 leafs per host. Using a list because it seems cleaner than leaf0, leaf1
lom = lights out management interface - always 1 per host
management - a dedicated interface accessible for management ssh, scp
stable = a stable bgp address on the loopback interface

I will have a further play with this today and see can I get it working.

Ah, interesting… so it sounds like “interfaces” would be just a single object (not a map of objects) because the different attributes of that object all represent different kinds of interface, and so it is the object itself that represents the collection of multiple interfaces.

In that case, it seems like the type constraint for interfaces should be just object({ ... }), and not map(object({ ... })), and then ignore my most recent example of making interfaces appear as a map in the actual value.

Thanks, I tried that just now it throws no errors. Hopefully this is on the right track now.

I had assumed I had to use

map(object({ ... })) ,

and that this means we are defining a map of objects of various types. Yours very confused.

Just wanted to say thank you again @apparentlymart for your help. I have been distracted and just got back to this today. Now that the hosts are defined in a map there is no need to flatten the data for for_each :slight_smile: This keeps things nice and simple. I have tested with null_resource and confident I can “plug” the each values into my real resources

resource "null_resource" "systems" {
  for_each = var.systems
  triggers = {
    name = each.key
    disk0 = each.value.disks[0]
    disk1 = each.value.disks[1]
    leaf0_device = each.value.interfaces["leafs"][0]["device"]
    leaf0_ip = each.value.interfaces["leafs"][0]["ip"]
    leaf0_netmask = each.value.interfaces["leafs"][0]["netmask"]
    leaf0_gateway = each.value.interfaces["leafs"][0]["gateway"]
    leaf1_device = each.value.interfaces["leafs"][1]["device"]
    leaf1_ip = each.value.interfaces["leafs"][1]["ip"]
    leaf1_netmask = each.value.interfaces["leafs"][1]["netmask"]
    leaf1_gateway = each.value.interfaces["leafs"][1]["gateway"]
    lom = each.value.interfaces["lom"]["ip"]
    management_device  = each.value.interfaces["management"]["device"]
    management_ip  = each.value.interfaces["management"]["ip"]
    management_netmask  = each.value.interfaces["management"]["netmask"]
    management_gateway  = each.value.interfaces["management"]["gateway"]
    management_mac  = each.value.interfaces["management"]["mac"]
    management_device  = each.value.interfaces["management"]["device"]
    stable_device  = each.value.interfaces["stable"]["device"]
    stable_ip  = each.value.interfaces["stable"]["ip"]
  }
}