Terraform Dynamic Block with conditional based on variable string values - terraform

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.

Related

Terraform Ternary Condition Working in Reverse

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.

puppet manifest check if variable is not empty

Following this existing link - Puppet how to tell if a variable is set. which is , below is the piece of the puppet manifest script :
if defined('$to_dir') {
notify { "Fourthvalue of $from_dir and $to_dir ... ":}
notify { "Fourth$to_dc... ":}
$worker_name = "acme${port}_${machine}${from_dir}_${to_dir}"
$system_id = "${machine}${from_dir}.${to_dir}
} else {
$worker_name = "acme${port}_${machine}_${pod}"
$system_id = $::fqdn
}
However, when we pass "to_dir" as null, it is still going into if block as actual(expect to be in else block).
Even tried using if $to_dir { or if $to_dir != undef { , this did not help.
The value of "to_dir" will be a word either "abc" or "def".
Please advise if something is wrong..
puppet manifest check if variable is not empty
You're throwing around a variety of terms -- "defined", "empty", "null" -- that mean different things or nothing in Puppet. But taken in toto, I think your purpose would be served by testing whether the variable in question is defined and contains a nonempty string. You can do that by matching the variable against an appropriate type expression. For example,
if $to_dir =~ String[1] {
# ...
}
That tests that variable $to_dir contains a string at least one character long. The condition will evaluate to false if $to_dir has not been assigned a value, or if it has been assigned a value of a type different from String, or if its value is an empty string. If the value is a string, it puts no other requirements on the contents. In particular, the value could consist only of one or more space characters.
i think the main issue here is that a variable which is instantiated with $var = undef is a defined variable with a value set to undef (its imo not well named). that said you checking the truthy state or undef (or using something specific to the datatype like size or empty) should all work. e.g.
# at this point defined('$var') == false
$var = undef
# now defined('$var') evalutes to false
# you should be able to check with the following that evaluate to true
($var == undef)
!$var
# you can also use the following which evaluates to false
$var =~ NotUndef

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
}

How to represent map with Groovy collectMaps

I have a Java code that looks like below code:
for(MyClass myclassObject: input.classes()) {
if(myclassObject.getName().equals("Tom")) {
outputMap.put("output", myclassObject.getAge())
}
}
How do I efficiently write this with Groovy collectmap?
I can do
input.classes().collectEntries["output":it.getAge()] But how do I include the if condition on it?
you could use findAll to keep only items according to condition
and after then apply collectEntries to transform items found
#groovy.transform.ToString
class MyClass{
int age
String name
}
def classes = [
new MyClass(age:11, name:'Tom'),
new MyClass(age:12, name:'Jerry'),
]
classes.findAll{it.getName()=='Tom'}.collectEntries{ [output:it.getAge()] }
Since your resulting map is only retaining one value anyway, you can also just do this:
input.classes().findResult { it.name == 'Tom' ? [output: it.age] : null }
where findResult will return the first item in classes() for which the closure:
{ it.name == 'Tom' ? [output: it.age] : null }
returns a non-null value.
Since you mentioned efficiency in your question: this is more efficient than going through the whole collection using collectEntries or findAll since findResult returns directly on finding the first instance of it.name == 'Tom'.
Which way to go really depends on your requirements.
collectEntries can take a closure as a parameter. You can apply your logic inside the closure and make sure you return the Map Entry when condition passes and return an empty map when condition fails. Therefore;
input.classes().collectEntries { MyClass myClassObject ->
myClassObject.name == 'Tom' ? ['output': myClassObject.getAge()] : [:]
}
However, with your approach there is a caveat. Since you are using the key as output and Map does not allow duplicate keys, you will always end up with the last entry in the map. You have to come up with a better plan if that is not your intention.

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.

Resources