Terraform Ternary Condition Working in Reverse - terraform

I have a Terraform module calling a submodule, which also calls another submodule. The final module uses a ternary condition as part of some logic to determine whether a dynamic block should be omitted in a resource definition.
I'm going to only include the pertinent code here, else it would get unnecessarily complicated.
The first module call:
module "foobar" {
source = "./modules/foobar"
...
vpc_cidr = "10.0.0.0/16"
# or vpc_cidr = null, or omitted altogether as the default value is null
...
}
The second module (in "./modules/foobar"):
module "second_level" {
source = "./modules/second_level"
...
vpc_config = var.vpc_cidr == null ? {} : { "some" = "things }
...
}
The third module (in "./modules/second_level"):
locals {
vpc_config = var.vpc_config == {} ? {} : { this = var.vpc_config }
}
resource "aws_lambda_function" "this" {
...
dynamic "vpc_config" {
for_each = local.vpc_config
content {
"some" = vpc_config.value["some"]
}
...
}
This is all horribly simplified, as I'm sure you're already aware, and you might have some questions about why I'm doing things like in the second level ternary operator. I can only say that there are "reasons", but they'd detract from my question.
When I run this, I expect the dynamic block to be filled when the value of vpc_cidr is not null. When I run it with a value in vpc_cidr, it works, and the dynamic block is added.
If vpc_cidr is null however, I get an error like this:
│ 32: security_group_ids = vpc_config.value["some"]
│ ├────────────────
│ │ vpc_config.value is empty map of dynamic
The really odd this is that if I swap the ternary around so it's actually the reverse of what I want, like this: vpc_config = var.vpc_config == {} ? { this = var.vpc_config } : {} everything works as I want.
EDIT
Some more context after the correct answer, because what I'm asking for indeed looks strange.
Wrapping this map into another single-element map with a hard-coded key if it's not empty
I was originally doing this because I needed to iterate just once over the map in the for_each block (and it contains more than a single key), so I'm faking a single key by putting a dummy key in there to iterate over.
As #martin-atkins points out in the answer though, for_each can iterate over any collection type. Therefore, I've simplified the locals assignment like this:
locals {
vpc_config = length(var.vpc_config) == 0 ? [] : [var.vpc_config]
}
This means that I can run a more direct dynamic block, and do what I really want, which is iterate over a list:
dynamic "vpc_config" {
for_each = local.vpc_config
content {
subnet_ids = var.vpc_config["subnet_ids"]
security_group_ids = var.vpc_config["security_group_ids"]
}
}
It's still a little hacky because I'm converting a map to a list of maps, but it makes sense more sense further up the chain of modules.

Using the == operator to compare complex types is very rarely what you want, because == means "exactly the same type and value", and so unlike many other contexts is suddenly becomes very important to pay attention to the difference between object types and map types, map types of different element types, etc.
The expression {} has type object({}), and so a value of that type can never compare equal to a map(string) value, even if that map is empty. Normally the distinction between object types and map types is ignorable because Terraform will automatically convert between them, but the == operator doesn't give Terraform any information about what types you mean and so no automatic conversions are possible and you must get the types of the operands right yourself.
The easiest answer to avoid dealing with that is to skip using == at all and instead just use the length of the collection as the condition:
vpc_config = length(var.vpc_config) == 0 ? {} : { this = var.vpc_config }
Wrapping this map into another single-element map with a hard-coded key if it's not empty seems like an unusual thing to be doing, and so I wonder if this might be an XY Problem and there might be a more straightforward way to achieve your goal here, but I've focused on directly answering your question as stated.
You might find it interesting to know that the for_each argument in a dynamic block can accept any collection type, so (unlike for resource for_each, where the instance keys are significant for tracking) you shouldn't typically need to create synthetic extra maps to fake conditional blocks. A zero-or-one-element list would work just as well for generating zero or one blocks, for example.

All of your code is behaving as expected. The issue here is that the dynamic block iterator is likely not being lazily evaluated at compilation, but rather only at runtime. We can workaround this by providing a "failover" value to resolve against for the situation when vpc_config.value is empty, and therefore has no some key.
content {
"some" = try(vpc_config.value["some"], null)
}
Since we do not know the specifics, we have to assume it is safe to supply a null argument to the some parameter.

Related

Terraform: Can I set variable type = any dict/object

Is there a way to allow any type of dictionary/object as an input variable?
I have a module to create cron expressions with lambda and I'm trying to add a variable to take in a dictionary to pass into the resource call.
I'd like just allow any dictionary of any length. With any type for the keys and values in it.
Initially, I tried just:
variable vars {
type = object
}
But that isn't allowed.
Right now I just have the type empty, so it will accept anything, but that doesn't seem like good practice.
Ideally this would be the complex type map(any) to specify it must be a map with any type nested. However, you state that:
I'd like just allow any dictionary of any length. With any type for the keys and values in it.
Unfortunately there is a stipulation that map(any) type for a variable declaration must have a consistent structure across all values for the entries. Therefore, you could use map(any) if the input structure is consistent like:
{
"one" = { "a_key" = "a_value", "another_key" = "another_value" },
"two" = { "a_key" = "value", "another_key" = "the_value" },
}
However an inconsistent structure:
{
"one" = { "another_key" = "another_value" },
"two" = { "a_key" = "value", "another_key" = 0 },
}
would force the any type, which is the least restrictive and what you stated you did not want, but it is your only option in that situation.
You can just it like this
variable vars {
type = any
}

Terraform Dynamic Block with conditional based on variable string values

I'm after a dynamic block only when a string var is a certain 2 values (stg or prod). This code doesn't work:
dynamic "log" {
for_each = var.environment == "stg" || var.environment == "prod" ? [1] : [0]
content {
category = "AppServiceAntivirusScanAuditLogs"
enabled = true
}
}
So I want this block when environment is "stg" or "prod" but don't want it when it is anything else. This runs but the logic doesn't work.
I've done something similar in the past using a boolean variable and this has worked but am reluctant to add another variable when I can surely evaluate these strings somehow?
Also tried moving the logic to the "enabled =" field which works but due to the nature of the issue I'm having, I need to do it at the block level.
Your conditionals for the ternary are correct, but the return values are not. When coding a ternary for an optional nested block, the "falsey" return value must be empty. This can be an empty set, list, object, or map type. The type does need to be consistent with your "truthy" return value. In your situation, you are returning a list type for the "truthy" value, so we need to return an empty list type for the "falsey" value:
dynamic "log" {
for_each = var.environment == "stg" || var.environment == "prod" ? [1] : []
content {
category = "AppServiceAntivirusScanAuditLogs"
enabled = true
}
}
As expected, there will be zero iterations on an empty value, which is the desired behavior for the return on the "falsey" conditional. As a side note, my personal preference is to return ["this"] for the "truthy" conditional on optional nested block ternaries to be consistent with recommended practices in Terraform around non-specific block naming conventions.

What is the key in complex data type map(object)

I am new to terraform and I am trying to understand the below code snippet.
Variable is of type map(objects) and Its looping over map(objects) and keys function takes a map and returns a list containing the keys from that map. Example: https://www.terraform.io/docs/configuration/functions/keys.html
I believe the output will be something like:
network_ids = {
network_alias = 123
network_alias = 456
network_alias = 789
}
What will be the value for network_alias? I went through many links but I am unable to understand.
code snippet:
locals {
network_ids = {
for network_alias in keys(var.networks) :
network_alias => aws_vpc.subnet[network_alias].id
}
}
variable "networks" {
type = map(object({
network_number = string
availability_zone = string
}))
}
I'm happy to break this down.
So, to begin with, network_ids is a terraform local value. Generally, local values are used to store off computations that you don't want to repeat over and over again.
network_ids is specifically a map for expression. map for expressions are used to build up maps from other, "enumerable" values.
In terraform a map is like a HashMap in other languages (dict in python, Hash in ruby, HashMap in Java and so on). map contains an association list mapping a unique key (always a string) to a value which could be of any, consistent type (meaning you can't have things like { "a" = 4, "b" = "c" } since 4 and "c" have different types).
In your specific example, for network_alias in keys(var.networks) says, basically
loop through the keys (again, strings) in var.networks and bind each one to the name network_alias.
The network_alias => aws_vpc.subnet[network_alias].id part says
build a new map where the key is the same as the keys we're looping over, and the value is a lookup of a subnet's ID indexed by the key.
That all being said, local.network_ids will not end up looking like the following, because maps have distinct keys and the comprehension actually evaluates the key value
network_ids = {
network_alias = 123
network_alias = 456
network_alias = 789
}
It's impossible for me to tell you exactly what the value will be because I don't know the value of var.networks nor that of aws_vpc.subnet.

terraform: performing a map operation on a list?

I have a terraform list
a = [1,2,3,4]
Is there a way for me to apply a function (e.g. *2) on the list, to get
b = [2,4,6,8]
I was looking for an interpolation syntax, perhaps map(a, _*2), or even something like
variable "b" {
count = "${length(a)}"
value = "${element(a, count.index)} * 2
}
As far as I can see no such thing exists. Am I missing something?
As per #Rowan Jacob's answer, this is now possible in v0.12 using the new for expression.
See: https://www.terraform.io/docs/configuration/expressions.html#for-expressions
variable "a" {
type = "list"
default = [1,2,3,4]
}
locals {
b = [for x in var.a : x * 2]
}
output "local_b" {
value = "${local.b}"
}
gives
Outputs:
local_b = [2, 4, 6, 8,]
This is currently an open issue. A new version of Terraform was recently announced which should give the ability to do this, among many other HCL improvements.
I think currently your best bet would be to create local values for each element of the list (remember that you can't use interpolation syntax in the default value of variables; locals exist to get around this limitation). However, I'm not sure if locals have a count attribute.

Cannot assign to 'X' in 'Y' in Swift

I have a dictionary with Structs in it. I am trying to assign the values of the struct when I loop through the dictionary. Swift is telling me cannot assign to 'isRunning' in 'blockStatus'. I haven't been able to find anything in the docs on this particular immutability of dictionaries or structs. Straight from the playground:
import Cocoa
struct BlockStatus{
var isRunning = false
var timeGapForNextRun = UInt32(0)
var currentInterval = UInt32(0)
}
var statuses = ["block1":BlockStatus(),"block2":BlockStatus()]
for (block, blockStatus) in statuses{
blockStatus.isRunning = true
}
cannot assign to 'isRunning' in 'blockStatus'
blockStatus.isRunning = true
This does work if I change the struct to a class.
I am guessing it has something to do with the fact that structs are copied and classes are always referenced?
EDIT: So even if it is copying it.. Why can't I change it? It would net me the wrong result but you can change members of constants just not the constant themselves. For example you can do this:
class A {
var b = 5
}
let a = A()
a.b = 6
Your guess is true.
By accessing blockStatus, you are creating a copy of it, in this case, it's a constant copy (iterators are always constant).
This is similar to the following:
var numbers = [1, 2, 3]
for i in numbers {
i = 10 //cannot assign here
}
References:
Control Flow
In the example above, index is a constant whose value is automatically set at the start of each iteration of the loop.
Classes and Structures
A value type is a type that is copied when it is assigned to a variable or constant, or when it is passed to a function. [...] All structures and enumerations are value types in Swift
Methods
Structures and enumerations are value types. By default, the properties of a value type cannot be modified from within its instance methods.
However, if you need to modify the properties of your structure or enumeration within a particular method, you can opt in to mutating behavior for that method. The method can then mutate (that is, change) its properties from within the method, and any changes that it makes are written back to the original structure when the method ends. The method can also assign a completely new instance to its implicit self property, and this new instance will replace the existing one when the method ends.
You can opt in to this behavior by placing the mutating keyword before the func keyword for that method:
You could loop through the array with an index
for index in 0..<statuses.count {
// Use your array - statuses[index]
}
that should work without getting "cannot assign"
If 'Y' in this case is a protocol, subclass your protocol to class. I had a protocol:
protocol PlayerMediatorElementProtocol {
var playerMediator:PlayerMediator { get }
}
and tried to set playerMediator from within my player mediator:
element.playerMediator = self
Which turned into the error cannot asign 'playerMediator' in 'element'
Changing my protocol to inherit from class fixed this issue:
protocol PlayerMediatorElementProtocol : class {
var playerMediator:PlayerMediator { get }
}
Why should it inherit from class?
The reason it should inherit from class is because the compiler doesn't know what kind your protocol is inherited by. Structs could also inherit this protocol and you can't assign to a property of a constant struct.

Resources