2
votes

I have a module that creates multiple resources for a list of names. So for each name supplied in a variable called instances, a set of resources (vm, ports, volumes) is created.
In the output of that module I want to have a map, that maps the instance (each value in instances) to the IP of an associated port.

This is the definition of the port

resource "openstack_networking_port_v2" "this" {
  for_each = var.instances

  name               = "port-${each.key}"
  network_id         = var.network_id
  admin_state_up     = true
  security_group_ids = var.security_group_ids
  fixed_ip {
    subnet_id = var.subnet_id
  }
}

Until now, I had this in the output of the module

output "int-port" {
  value     = openstack_networking_port_v2.this
}

and this where I used it

int-ip : module.my-instance.int-port["${var.deployment}-my-01"].all_fixed_ips[0]

After upgrading terraform, I need to add sensitive = true to the output, as the openstack provider has marked something as sensitive, which will lead to the output not being printed. (I know I can get it with terraform output -json)
So I want to just return the IPs I need in the output instead of the whole object, but I can't figure out how.

I tried the following things:

output "int-ip" {
  value = openstack_networking_port_v2.this.*.all_fixed_ips[0]
}

and

output "int-ip" {
  value = openstack_networking_port_v2.this[*].all_fixed_ips[0]
}

which gives me

│ Error: Unsupported attribute
│
│   on ../../modules/ext-instance-v2/outputs.tf line 24, in output "int-port":
│   24:   value = openstack_networking_port_v2.this.*.all_fixed_ips[0]
│
│ This object does not have an attribute named "all_fixed_ips".

I also tried

output "int-ip" {
  value = {
    for instance in var.instances:
    instance => openstack_networking_port_v2.this["${instance}"].all_fixed_ips[0]
  }
}

and

output "int-ip" {
  value = {
    for instance in var.instances:
    instance => openstack_networking_port_v2.this[instance].all_fixed_ips[0]
  }
}

which leads to that error

│ Error: Invalid index
│ 
│   on ../../modules/ext-instance-v2/outputs.tf line 19, in output "int-ip":
│   19:     instance => openstack_networking_port_v2.this["${instance}"].all_fixed_ips[0]
│     ├────────────────
│     │ openstack_networking_port_v2.this is object with 9 attributes
│ 
│ The given key does not identify an element in this collection value.

It feels like I'm very close, but just out of reach.
I'm not only interested in the solution, but also in an explanation why the things I tried, did not work out.

2

2 Answers

2
votes

Since your openstack_networking_port_v2.this is map due to for_each, it should be:

output "int-ip" {
  value = values(openstack_networking_port_v2.this)[*].all_fixed_ips[0]
}

Update

Based on the comments. The correct way is for the last attempt is:

output "int-ip" {
  value = {
    for instance in keys(var.instances):
      instance => openstack_networking_port_v2.this[instance].all_fixed_ips[0]
  }
}

This is required, as instances is a map, but you need a list. In this case, you want to use the list of keys in the for-each.

1
votes

The [*] operator (Splat operator) is designed for producing lists only. The documentation says the following about splat expressions on maps:

The splat expression patterns shown above apply only to lists, sets, and tuples. To get a similar result with a map or object value you must use for expressions.

Resources that use the for_each argument will appear in expressions as a map of objects, so you can't use splat expressions with those resources. For more information, see Referring to Resource Instances.

The simplest version of this with for expressions would be the following:

output "int-ip" {
  value = {
    for k, port in openstack_networking_port_v2.this : k => port.all_fixed_ips[0]
  }
}

This means to construct a new mapping where each element of openstack_networking_port_v2.this (a map of objects) is translated into an element that has the same key but has just the first IP address as the value.

An important difference in my example above vs. the ones in your question is that the for clause has two symbols: k, port. This means to put the key of each element in k and the value of each element in port. If you write only a single symbol, like for port, then Terraform will bind that to the value of each element rather than the key, and there is no straightforward way to find the key corresponding to a particular value, only to find a value corresponding to a key.

It would also be possible in principle to use the keys function to repeat over the keys in particular, and then use the key to look up the element:

output "int-ip" {
  value = {
    for k in keys(openstack_networking_port_v2.this) : k => openstack_networking_port_v2.this[k].all_fixed_ips[0]
  }
}

This is equivalent to the other for expression I showed above, but is more verbose due to needing to refer to openstack_networking_port_v2.this twice. Therefore I prefer the two-symbol for, particularly in cases like this where the expression being mapped has a large number of characters in it.