"Ambiguous attribute key" error after updating to terraform version 0.12.26 - terraform

After terraform version update from 0.11 to 0.12.26, I'm seeing error with lookup and list of values inside map.
variable "foo" {
type = map
}
foo = {
x.y = "bar"
}
I have a map "foo" as variable type (map) and then i have key-value pair in map with x.y = "bar". In lookup, I'm trying to read value of x.y as,
lookup(var.foo, x.y)
with this, I'm getting error,
Error: Ambiguous attribute key
on line 13:
13: x.y = "bar"
If this expression is intended to be a reference, wrap it in parentheses. If
it's instead intended as a literal name containing periods, wrap it in quotes
to create a string literal.
can someone help?

If you want to have a map key that contains a dot character . then you must write the key in quotes, so Terraform can see that you intend to produce a string containing a dot rather than to use the value of the y attribute of variable x:
foo = {
"x.y" = "bar"
}
Likewise, to access that element you'll need to quote the key in the index expression, like foo["x.y"]. You could also potentially use lookup(foo, "x.y") -- still with the quotes -- but that approach is deprecated in Terraform 0.12 because foo["x.y"] has replaced it as the main way to access an element from a map value.

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.

Terraform 12 var and string concatenation best practice

I'm updating from terraform 0.11 to 0.12 and I was wondering what was the "best practice" to concatenate string and vars in my .tf files.
The new syntax is pretty straightforward reguarding the variables :
# V0.11
foo = "${var.bar}"
# V0.12
foo = var.bar
but how should-I handle this situation ?
foo = "${var.bar}-a-string"
Shall-I keep this syntax or turn it in something like :
foo = join("-", [${var.bar}, "a", "string"])
This guy seems to think we should keep interpolation syntax for string concatenation even if it's deprecated in the new terraform version.
To concatenate variable with the string, use this syntax instead of join() :
foo = "string-${var.bar}-a-string"
But if you don't want to use a variable for string concatenation, you can use such syntax:
foo = var.bar

how to get the raw version of a template string in iojs

Is it possible to get the raw version of a template string in iojs ?
var s = `foo${1+1}bar`
console.log(s); // foo2bar
In the previous example I would like to get the string: foo${1+1}bar
edit1:
My need is to detect whether a template string depends on its context of if is is just a 'constant' string that may contain CR and LF
Is it possible to get the raw version of a template string in iojs ?
No it is not. It's not possible to get the raw representation of the literal, just like there is no way to get the "raw" literal in these cases:
var foo = {[1+1]: 42};
var bar = 1e10;
var baz = "\"42\"";
Note that the term "template string" is misleading (as it may indicate that you could somehow get the raw value of the string (which is also not the case as shown above)). The correct term is "template literal".
My need is to detect whether a template string depends on its context of if is is just a 'constant' string that may contain CR and LF
Seems like a job for a static analysis tool. E.g. you can use recast to parse the source code and traverse all template literals.
For example, the AST representation of `foo${1+1}bar` is:
If such an AST node as an empty expression property, then you know that the value is constant.
There is a way to determine whether a template literal is "static" or "dynamic" at runtime, but that involves changing the behavior of the code.
You can use tagged templates. Tagged templates are functions that get passed the static and dynamic portions of a template literal.
Example:
function foo(template, ...expressions) {
console.log(template, expressions);
}
foo`foo${1+1}bar` // logs (["foo", "bar"], [2]) but returns `undefined`
I.e. if foo gets passed only a single argument, the template literal does not contain expressions. However, foo would also have to interpolate the static parts with the dynamic parts and return the result (not shown in the above example).

Fluent Groovy syntax using command chaining and maps

Given the following Groovy code:
someMap = ['key':{ str -> println "SUCCESS: ${str}" }]
clos = { someMap }
All of the following are legal ways to print SUCCESS: abc:
clos(null)['key'] "abc"
clos null key "abc"
someMap['key'] "abc"
This one, however:
someMap key "abc"
throws a groovy.lang.MissingPropertyException: No such property: key for class: ConsoleScript50.
If clos(null) and someMap both resolve to java.util.LinkedHashMap, then what makes clos null key "abc" legal, but someMap key "abc" not legal?
Groovy understands someMap key "abc" as someMap(key).getAbc(), which won't work. You really need the dot:
someMap.key "abc"
Or square brackets
someMap['key'] "abc"
Update
It seems that Groovy will always disambiguate a token like key as a call parameter (i.e. someMap(key)) if it can.
Yes, it will.
Only if that interpretation doesn't make sense does it instead interpret the token as a property (.key) or map dereference (['key']).
No, Groovy will always understand the second parameter, without dots or parens, as a call parameter. This
function parameter
Is always undertood as
function(parameter)
It features no "precedence" over object[key]
If you keep adding stuff without dots or parenthesis to disambiguate, Groovy will keep adding parens and dots as per it's own rules. This:
gimme coffee with sugar and milk
Is understood as
gimme(coffee).with(sugar).and(milk)
Also this:
clos null key 'abc'
Will always be undertood as
clos(null).key('abc')
because it translates to someMap.key()..., and there is no such method (only a property (via missing). someMap.key "abc" also works.
the other call translates to clos(null).key("abc"), which first derefs into the map and then just calls call on the result.
def x = clos null key
assert x.is(someMap.key)

does Template Haskell name quoting desugar 'x to NameG?

Can I always expect the single single-quote syntax to desugar to the NameG constructor? e.g. does
'x
always desugar to
(Name (OccName "x") (NameG VarName (PkgName "some-package") (ModName "SomeModule")))
This information must always be there, after name resolution, which is the stage Template Haskell runs after, right? And I haven't been able to quote local names, though I'm only interested in quoting top-level names.
Context: I want to write a function that returns the uniquely-qualified identifier. It's a partial function because I can't constrain the input, as Template Haskell doesn't have any GADTs or anything, while I don't want to wrap the output in uncertainty. And I don't want to use a quasi-quoter or splice, if ' will do. I want to prove that this partial function is safe at runtime when used as above, quoting top-level names in the same module, given:
name (Name occ (NameG _ pkg mod)) = Unique occ pkg mod
I want to have a function like:
(<=>) :: Name -> a -> Named a
given:
data Named a = Named a Unique
to annotate variable bindings:
x = 'x
<=> ...
without the user needing to use the heavy splice syntax $(name ...), and invoke splicing at compile time:
x = $(name 'x)
<=> ...
The user will be writing a lot of these, for configuration.
https://downloads.haskell.org/~ghc/7.8.3/docs/html/users_guide/template-haskell.html and https://hackage.haskell.org/package/template-haskell-2.8.0.0/docs/src/Language-Haskell-TH-Syntax.html#Name didn't say.
(p.s. I'd also like to know if the double single-quote syntax (e.g. ''T) had the analogous guarantee, though I'd expect them to be the same).
Since ' quoted names are known at compile time, why don't you change name to be in the Q monad:
name :: Name -> ExpQ
name (Name occ (NameG _ pkg mod)) = [| Unique occ pkg mod |]
name n = fail $ "invalid name: "++ gshow n
Then you use $(name 'show) :: Unique instead of name 'show :: Unique. If you get an invalid Name (say somebody uses mkName), that failure will show up at compile time.

Resources