Get the index of a set inside of a dynamic block - terraform

I'm trying to get the index of a set. Look here for examples: Terraform get list index on for_each
This doesn't seem to work in a dynamic block though. The key and value are the same value still. How do I get the index number of an item in the set in a dynamic block?
dynamic "set" {
for_each = { for idx, item in var.myset: item => idx}
content {
name = "Company----${set.key}"
value = "Company----${set.value}"
}
}

You can't get the index number of an item in a set per Terraform's documentation on dynamic blocks.
key is the map key or list element index for the current element. If the for_each expression produces a set value then key is identical to value and should not be used.
It's probably a smell that there is better way you could approach this problem. But not knowing the full scope of what you are trying to achieve, I also understand that sometimes Terraform requires us to jump through some interesting hoops.
If you are dead set on getting the set index and its values in a dynamic block like you have above then you can convert the set to a map assigning indexes as the keys.
Something like this should work.
zipmap(range(length(var.myset)), var.myset)
Then you can use the resulting map in your for_each block. That should make set.key yield the current index (that was constructed by the range function) and set.value yield the value that was contained in the set.
An important thing to note is that unlike lists, sets are not ordered, so they don't have an index, per se. That's why you can't use the index function like the example above. It's also why I built the indexes using the range function.
It's also possible that you may have to explicitly convert the set to a list - Terraform should do an implicit conversion. But sometimes what it should do and what it does don't always align. And in that case, the statement would be as follows.
zipmap(range(length(var.myset)), toList(var.myset))
This is also explained in Terraform's documentation on types.

Related

Is it possible to recover from an error returned by a data source?

Let's take the dns_a_record_set data source, for example, if one does:
data "dns_a_record_set" "test" {
## Purposely passing a DNS that would not resolve
host = "thisDnsDoesNotResolve.org"
}
output "test" {
value = data.dns_a_record_set.test.addrs
}
The error returned is, as expected:
Error: error looking up A records for "thisDnsDoesNotResolve.org": lookup thisDnsDoesNotResolve.org on 1.1.1.1:53: no such host
│
│ with data.dns_a_record_set.test,
│ on main.tf line 1, in data "dns_a_record_set" "test":
│ 1: data "dns_a_record_set" "test" {
But, is it possible to recover from this error and have the test output assigned with a default value?
A naive trial of mine was to use the try function, but that obviously does not work, as the error does not happens when data is accessed, but rather when fetched, as shown in the error above.
output "test" {
## Seems like it doesn't even reach here
value = try(data.dns_a_record_set.test.addrs, ["127.0.0.1"])
}
So, is this even possible to recover from this error?
And, then, how?
The purpose of data resources in Terraform is to declare dependencies on objects managed elsewhere and, as a side-effect, obtain information about them to use in other parts of your module in a similar way as you might with objects that are managed by your current module.
If your module only depends on this DNS record in certain situations then a typical approach would be to use either the count or for_each meta-argument to explicitly declare in which situations your module depends on this object and which situations it doesn't.
It's hard to show a specific example without more information about your underlying motivation for this, but here's a contrived example where the hostname can be directly provided by the module caller as an input variable, and the dependency on that external object is only relevant when that variable is set (non-null):
variable "hostname" {
type = string
default = null # this variable is optional
}
data "dns_a_record_set" "test" {
count = length(var.hostname[*])
host = var.hostname
}
output "ip_addresses" {
value = one(data.dns_a_record_set.test[*].addrs)
}
The above uses two additional language features I didn't mention yet in this comment, which I'll describe briefly for completeness:
Applying the splat operator [*] to a non-list value converts the value to be a list which either has one element (if the value is non-null) or zero elements (if the value is null). Therefore there will be one instance of this data resource if and only if var.hostname is non-null.
The one function does essentially the opposite of that: if given a one-element list it will return the first element, and if given a zero-element list it will return null.
These two features taken together allow you to concisely pivot between values that might be null and lists of zero or one element, which is useful in situations like this where the "null-ness" of a valid decides how many of something should be declared.
Note that the try function only "catches" expression evaluation failures. It isn't relevant to this situation because what failed was the declared dependency on the external object; the error was returned when Terraform asked the provider to retrieve the dns_a_record_set object, not in the downstream expression which referred to that.
(Indeed, Terraform would never actually evaluate that expression in your case, because when there's a dependency from object B to object A, a failure of object A halts processing before reaching object B.)

module.db is a list of object, known only after apply

rds.tf:-
module "db" {
**count = var.environment == "dev" || var.environment == "qa" ? 1 : 0**
source = "../rds"
identifier = var.db_name
engine = var.rds_engine
engine_version = var.rds_engine_version
output.tf:
output "rds_instance_endpoint" {
description = "The connection endpoint"
value = module.db.db_instance_endpoint
}
ERROR:-
Error: Unsupported attribute
on outputs.tf line 28, in output "rds_instance_endpoint":
28: value = module.db.db_instance_endpoint
module.db is a list of object, known only after apply
Can't access attributes on a list of objects. Did you mean to access attribute "db_instance_endpoint" for a specific element of the list, or across all elements of the list?
getting the above error while declaring count in the rds.tf module.
if I remove count then its working fine, not sure what is this error.
The error message in this case is the sentence "Can't access attributes on a list of objects", not the sentence "module.db is a list of object, known only after apply"; the latter is just additional context to help you understand the error message that follows.
In other words, Terraform is telling you that module.db is a list of objects and that it's invalid to use .db_instance_endpoint ("accessing an attribute") on that list.
The root problem here is that you have a module object that may or may not exist, and so you need to explain to Terraform how you want to handle the situation where it doesn't exist.
One answer would be to change your output value to return a set of instance endpoints that would be empty in environments other than the dev and QA environments:
output "rds_instance_endpoints" {
description = "The connection endpoints for any database instances in this environment."
value = toset(module.db[*].db_instance_endpoint)
}
Here I used the "splat" operator to take the db_instance_endpoint attribute value for each instance of the module, which currently means that it will either be a single-element set or an empty set depending on the situation. This approach most directly models the underlying implementation and would give you the freedom to add additional instances of this module in future if you need to, but you might consider the fact that this is a multi-instance module to be an implementation detail which should be encapsulated in the module itself.
The other main option, which does hide that implementation detail of the underlying module being a list, would be to have your output value be null in the situation where there are no instances of the module, or to be a single string when there is one instance. For that, we can slightly adapt the above example to use the one function instead of the toset function:
output "rds_instance_endpoints" {
description = "The connection endpoint for the database instance in this environment, if any."
value = one(module.db[*].db_instance_endpoint)
}
The one function has three possible outcomes depending on the number of elements in the given collection:
If the collection has no elements, it will return null.
If the collection has one element, it will return just the value of that element.
If the collection has more than one element, it will fail with an error message. But note that this outcome is impossible in your case, because count can only be set to zero or one.
Null values can potentially be annoying to deal with if this data is being returned to a calling module for use elsewhere, but sometimes it's the most reasonable representation of the described infrastructure and so worth that additional complexity. For root module output values in particular, Terraform will treat a null value as if the output value were not set at all, hiding it in the messaging from terraform apply, and so this second strategy is often the best choice in situations where your primary motivation is to return this information in the user interface for a human to use for some subsequent action.

When is the equal sign (=) required in Terraform when assigning a map (TF 0.11)

I wasn't able to solve this question by any other method, so I have to ask this here...
What is the logic behind using the equal sign (=) when assigning a map to a value in Terraform 0.11.
With =
resource "pseude_resource" "pseudo_name" {
value = {
key1 = value1
key2 = value2
}
}
Without =
resource "pseude_resource" "pseudo_name" {
value {
key1 = value1
key2 = value2
}
}
The = seems to be required when using arrays ([]), but isn't when using a map. What is the reason behind this and why on earth?? Can it just be omited?
In the Terraform language, there are two distinct constructs that have quite similar-looking syntax in the common case.
Arguments that expect maps
An argument in Terraform is a single name/value pair where the provider dictates (in the resource type schema) what type of value it expects. You are free to create a value of that expected type any way you like, whether it be as a literal value or as a complex expression.
The argument syntax is, in general:
name = value
If a particular argument is defined as being a map then one way you can set it is with a literal value, like this:
tags = {
Name = "foo bar baz"
}
...but you can also use a reference to some other value that is compatible with the map type:
# Tags are the same as on some other VPC object
tags = aws_vpc.example.tags
...or you can combine maps together using built-in Terraform functions:
tags = merge(local.default_tags, var.override_tags)
Generally speaking, you can use any expression whose result is a map with the expected element type.
In Terraform 0.11 these non-literal examples all need to be presented in the template interpolation syntax ${ ... }, but the principle is the same nonetheless.
Nested blocks
Whereas an argument sets some specific configuration setting for the object whose block it's embedded in, the nested block syntax conventionally declares the existence of another object that is related to the one the block is embedded in. Sometimes this really is a separate physical object, such as a rule associated with a security group, and other times it's a more conceptual "object", such as versioning in aws_s3_bucket which models the versioning feature as a separate "object" because the presence of it activates the feature.
The nested block syntax follows the same conventions as the syntax for the top-level resource, variable, terraform, etc blocks:
block_type "label" {
nested_argument = value
}
Each block type will have a fixed number of labels it expects, which in many cases is no labels at all. Because each block represents the declaration of a separate object, the block structure is more rigid and must always follow the shape above; it's not possible to use arbitrary expressions in this case because Terraform wants to validate that each of the blocks has correct arguments inside it during its static validation phase.
Because the block syntax and the map literal syntax both use braces { }, they have a similar look in the configuration, but they mean something quite different to Terraform. With the block syntax, you can expect the content of the block to have a fixed structure with a specific set of argument names and nested block types decided by the resource type schema. With a map argument, you are free to choose whichever map keys you like (subject to any validation rules the provider or remote system might impose outside of the Terraform schema).
Recognizing the difference
Unfortunately today the documentation for providers is often vague about exactly how each argument or nested block should be used, and sometimes even omits the expected type of an argument. The major providers are very large codebases and so their documentation has variable quality due to the fact that they've been written by many different people over several years and that ongoing improvements to the documentation can only happen gradually.
With that said, the provider documentation will commonly use the words "nested block" or "block type" in the description of something that is a nested block, and will refer to some definition elsewhere on the page for exactly which arguments and nested blocks belong inside that block. If the documentation states that a particular argument is a map or implies that the keys are free-form then that suggests that it's an argument expecting a map value. Another clue is that block type names are conventionally singular nouns (because each block describes a single object) while arguments that take maps and other collections conventionally use plural nouns.
If you find specific cases where the documentation is ambiguous about whether a particular name is a nested block type or an argument, it can be helpful to open an issue for it in the provider's repository to help improve the documentation. Terraform's documentation pages have an "Edit This Page" link in the footer which you can use to propose simple (single-page-only) edits as a pull request in the appropriate repository.
The longer-form explanation of these concepts is in the documentation section Arguments and Blocks.
The confusion comes from the behavior of the hcl language used by terraform. The functionality isn't very well documented in terraform but... In hcl, you can define a list by using repeating blocks, which is how resources like aws_route_table define inline routes, e.g.
resource "aws_route_table" "r" {
vpc_id = "${aws_vpc.default.id}"
route {
cidr_block = "10.0.1.0/24"
gateway_id = "${aws_internet_gateway.main.id}"
}
route {
ipv6_cidr_block = "::/0"
egress_only_gateway_id = "${aws_egress_only_internet_gateway.foo.id}"
}
tags = {
Name = "main"
}
}
which would be equivalent to
resource "aws_route_table" "r" {
vpc_id = "${aws_vpc.default.id}"
route = [
{
cidr_block = "10.0.1.0/24"
gateway_id = "${aws_internet_gateway.main.id}"
},
{
ipv6_cidr_block = "::/0"
egress_only_gateway_id = "${aws_egress_only_internet_gateway.foo.id}"
}
]
tags = {
Name = "main"
}
}
You want to make sure you're using the = when you're assigning a value to something, and only use the repeating block syntax when you're working with lists. Also, from my experience I recommend NOT using inlines when an individual resource is available.
Some very limited documentation for hcl can be found in the readme for the repo.

How to refer to alternate resources in Terraform?

Terraform (as of today) has the somewhat disturbing limitation, that you cannot create a resource with an interpolated (calcuted) lifecycle attribute prevent_destroy.
Terraform: How to get a boolean from interpolation?
https://github.com/hashicorp/terraform/issues/3116
The work-around is pretty simple to code, just create 2 resources with "alternating" counts. When you have 1 "production" resource which does not allow destroying you have 0 "testing" resources which can be destroyed. Or the other way round. (See the answer to the stackoverflow question linked
above for details.)
However, this brings up a new question. When I want to refer to "the one of the alternate resources that exists for this execution", how do I do that?
In pseudo code something like
"${local.production ? "${aws_eip.commander_production.public_ip}" : "${aws_eip.commander_testing.public_ip}" }"
This pseudo code cannot work for a couple of reasons:
aws_eip.commander_production is no longer a single resource, it is a list, so you need the * syntax
one of the lists is always empty and Terraform easily complains that it cannot determine the type of an empty list. (I guess because the ternary operator requires that the alternates have the same type)
when you access into an empty list you will get an error (With C semantics the unused alternate would not be evaluated, but Terraform seems to work differently and I got errors when trying to code this)
To work around those I came up with the following hacky solution:
Extend the lists with a dummy element in the end and then refer to the
first element of the extended list. The code for this is pretty
horrible to type, but it seems to work
locals {
dummy = [ "foo" ]
}
output "0101: address" {
value = "${format("Public IP is %s", "${local.production ? "${element("${concat("${aws_eip.commander_production.*.public_ip}", "${local.dummy}")}", 0)}" : "${element("${concat("${aws_eip.commander_testing.*.public_ip}", "${local.dummy}")}", 0)}" }")}"
}
Question: What is a shorter / more elegant way to code this?
Note: I have found one answer myself, but feel welcome to contribute even better ones.
A bit shorter code is
output "0101: address" {
value = "${format("Public IP is %s", "${element("${concat("${aws_eip.commander_production.*.public_ip}", "${aws_eip.commander_testing.*.public_ip}")}", 0)}")}"
}
In plain text: Concatenate the lists and take the first element of the the result. One list has one element and the other one zero, so the result will be what we want, regardless whether the element is in the first or second list.

Is it possible to take the name of a variable and turn it into a string in ActionScript 3.0?

I am making a simple debugger window in ActionScript for myself where I can add and remove variables I want to track. I was to be able to add variables to the list by just doing something like
DebuggerMonitor.trackVar(variable).
My question is, is there any way I can turn "variable" itself (the name, not the value) into a String to be added into a text field?
Depending on how "intelligent" your debugger should be, you could just pass the name along:
DebuggerMonitor.trackVar( variable, "variable" );
since obviously, when used in a context like this, the name should be known at the time you are writing the program.
You can also do some reflection magic to get instance variable names, but it won't work for temp variables (their names are dropped at compilation time):
public function getVariableName( instance:*, match:* ):String {
var typeDescription:XML = describeType( instance );
var variables:XMLList = typeDescription..variable;
var accessors:XMLList = typeDescription..accessor;
for each(var variable:XML in variables)
if(matchesXMLName( instance, variable, match ))
return variable.#name;
for each(var accessor:XML in accessors)
if(matchesXMLName( instance, accessor, match ))
return accessor.#name;
return "No name found.";
}
private function matchesXMLName( instance:*, xml:XML, match:* ):Boolean {
return match == instance[xml.#name.toString()];
}
var varName:String = getVariableName ( myObject, variable );
Using reflections like this will also be quite costly, if used often - you will have to think of a way to cache the type descriptions.
I recommend you check out the as3commons reflections package - there is a lot of useful functionality in there...
Short answer - No :(
You can access the type name but not individual instance names, as these are lost at run-time.
There is a confusion caused by the keyword 'var' because it is used to create several types of bindings.
Lexical bindings (the keyword 'var' was used inside a function).
Dynamic bindings (the keyword 'var' was used to declare a class' field).
Lexical bindings are interpreted by the compiler at compile time as addresses of the registers of the registers space occupied by the function. The names given to lexical bindings perish at this time and it is not possible to restore them at runtime - therefore you can't get the "name" of the variable.
Dynamic bindings are a kind of "public API" of the objects that declare them, they may be accessed from the code that was not compiled together with the code that created them, this is why, for the purpose of reflection the names of these bindings are stored in compiled code. However, ActionScript has no way of referencing LHS values, so you cannot, even if you know the name of the variable and the object declaring it, pass it to another function. But you can look it up in the debugger or by calling describeType on the object declaring the variable. Note that describeType will not show information on private variables even if you are calling it from the scope of the object in question.

Resources