Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform only partially creates object in for loop #27966

Closed
elliott-weston-cko opened this issue Mar 3, 2021 · 4 comments · Fixed by #28116
Closed

Terraform only partially creates object in for loop #27966

elliott-weston-cko opened this issue Mar 3, 2021 · 4 comments · Fixed by #28116
Assignees
Labels
bug config confirmed a Terraform Core team member has reproduced this issue dependencies Auto-pinning explained a Terraform Core team member has described the root cause of this issue in code v0.14 Issues (primarily bugs) reported against v0.14 releases

Comments

@elliott-weston-cko
Copy link

elliott-weston-cko commented Mar 3, 2021

Terraform Version

❯ tf14 version
Terraform v0.14.7

Terraform Configuration Files

main.tf

locals {

  ## We need to create the cartesian product of a whole bunch of variables, so we are chaining for loops.
  ## We also need to constantly update the name, to ensure sufficient uniqueness in the for_each loop

  subfolders = [for s in var.subfolders : length(s) == 0 ? s : trimprefix(s, "/")]
  initial_stack_set = flatten([for b in var.branches : [for s in local.subfolders: { name = join("/", compact([var.repository, b, s])), branch = b, subfolder = s }]])

  name_prefix_formatting = var.name_prefixes == null ? null : [for n in var.name_prefixes : trimprefix(n, "/")]
  name_prefix_stack_set = local.name_prefix_formatting == null ? local.initial_stack_set : flatten([for s in local.initial_stack_set : [ for n in local.name_prefix_formatting : { name = "${n}/${s.name}", branch = s.branch, subfolder = s.subfolder}]])

  workspace_stack_set = var.terraform_workspaces == null ? local.name_prefix_stack_set : flatten([for s in local.name_prefix_stack_set : [ for w in var.terraform_workspaces : { name = "${s.name}:${w}", branch = s.branch, subfolder = s.subfolder, workspace = w}]])

  stack_map = { for s in local.workspace_stack_set : s.name => s }

}

resource "null_resource" "this" {
  for_each = local.stack_map
}

output "my_map" {
  value = local.stack_map
}

variables.tf

variable repository {
  type = string
}

variable branches {
  type = list(string)
  default = ["main"]
}

variable subfolders {
  type = list(string)
  default = [""]
}

variable name_prefixes {
  type = list(string)
  default = null
}

variable terraform_workspaces {
  type = list(string)
  description = "Terraform workspaces"
  default = null
}

terraform.tfvars

repository = "my-repo"
branches = ["main"]
subfolders = ["vpc", "ecs", "route53"]
name_prefixes = ["a-team", "b-team"]
terraform_workspaces = ["dev","qa","sbox"]

Debug Output

https://gist.github.com/elliott-weston-cko/b898d186ade9e54949bc6f8d1be5225b

Expected Behavior

All the attributes defined in local.workspace_stack_set should be created

Actual Behavior

The workspace attribute is missing

Steps to Reproduce

  1. Copy the config I've added to a local folder
  2. terraform init
  3. terraform plan

Additional Context

I'm trying to create the cartesian product of multiple variables, to do this I am chaining for-loops, and then finally creating a map with the name as the key (intention is for it to be used in a for_each loop).
If you perform the reproduction steps, you'll notice a few things.

The "final" set that is created has four attributes:

  • name
  • branch
  • subfolder
  • workspace

But in the output the workspace attribute is missing.
Initial thought was the for-loop that creates the workspace wasn't performing any iterations. However, the workspace is being evaluated as the key of the map contains the workspace attribute.
https://gist.github.com/elliott-weston-cko/9010a41a5fcb619b916c74f7cd54d4fb

To further confusion if the name_prefixes tfvars are commented out, so it has its default value of null, then the workspace attribute will appear in the output.
https://gist.github.com/elliott-weston-cko/8f8e6941fbd4bbeb7b2f608032e3558a

References

@elliott-weston-cko elliott-weston-cko added bug new new issue not yet triaged labels Mar 3, 2021
@jbardin jbardin added confirmed a Terraform Core team member has reproduced this issue dependencies Auto-pinning and removed new new issue not yet triaged labels Mar 3, 2021
@jbardin
Copy link
Member

jbardin commented Mar 3, 2021

Thanks for filing the issue @elliott-weston-cko! This one was quite the puzzle, since the behavior seemed to be indicating that the incorrect conditional was being chosen in some situations. That however was a red herring, as the underlying problem turned out to be the unification of object types within the conditionals in the upstream hcl library.

What is actually going on is that when the conditional expression is attempting to evaluate the true and false arguments, a single result type must first be determined based in the arguments. In one case we have an object with the workspace attribute, and in the other a similar object without a workspace attribute. Due to the fact that objects can be assigned to a type containing a subset of identical attributes, the unification process determines that the object without the workspace attribute is the correct type to return. This means that the workspace values which were just added are dropped by the conditional expression. (This can be confounded by different rules for unifying tuple and list types as well, but in this case only the objects were interfering with the expected result).

Any fix here would need to come from the upstream hcl or cty libraries, but in the meantime we can workaround the issue by explicitly declaring the types we want to operate on. In this case, wrapping all object literals in tomap will ensure that only maps are used and object conversion rules do not apply

  subfolders = [for s in var.subfolders : length(s) == 0 ? s : trimprefix(s, "/")]
  initial_stack_set = flatten([for b in var.branches : [for s in local.subfolders: tomap({ name = join("/", compact([var.repository, b, s])), branch = b, subfolder = s })]])

  name_prefix_formatting = var.name_prefixes == null ? null : [for n in var.name_prefixes : trimprefix(n, "/")]
  name_prefix_stack_set = local.name_prefix_formatting == null ? local.initial_stack_set : flatten([for s in local.initial_stack_set : [ for n in local.name_prefix_formatting : tomap({ name = "${n}/${s.name}", branch = s.branch, subfolder = s.subfolder})]])

  workspace_stack_set = var.terraform_workspaces == null ? local.name_prefix_stack_set : flatten([for s in local.name_prefix_stack_set : [ for w in var.terraform_workspaces : tomap({ name = "${s.name}:${w}", branch = s.branch, subfolder = s.subfolder, workspace = w})]])

  stack_map = { for s in local.workspace_stack_set : s.name => s }

@jbardin jbardin added the explained a Terraform Core team member has described the root cause of this issue in code label Mar 3, 2021
@elliott-weston-cko
Copy link
Author

Thank you for the prompt and in-depth explanation @jbardin 🙇🏽‍♂️
The explanation makes a lot of sense around why I was seeing the behaviour described, and can confirm the tomap function resolves this issue.

There is one slight nuance that is still puzzling me though
I don't understand why when name_prefixes is set to null the workspace attribute is not removed.

I appreciate the terraform code is not the easiest to read, so I'm just going to walk through the thing that's puzzling me.

name_prefix_formatting = var.name_prefixes == null ? null : [for n in var.name_prefixes : trimprefix(n, "/")]
name_prefix_stack_set = local.name_prefix_formatting == null ? local.initial_stack_set : flatten([for s in local.initial_stack_set : [ for n in local.name_prefix_formatting : { name = "${n}/${s.name}", branch = s.branch, subfolder = s.subfolder}]])

In these 2 lines we are doing some basic formatting, and then a null check. If the null check is true, use the values from local.initial_stack_set, if not create the product of combining the initial_stack_set with the name_prefixes.

In both scenarios, the final object that is assigned to the local name_prefix_stack_set is a list of objects with the format:

{
  name = <name>,
  branch = <branch>,
  subfolder = <subfolder>
}

The only difference being there are more objects in the list if the null check is false.

So when the workspace_stack_set local is computed, the local name_prefix_stack_set will have the same structure regardless of whether we actually created the product between the name_prefixes and the initial_stack_set. They are both a subset of identical attributes when compared to the structure created in the workspace for loop evaluation.

I don't understand why in one scenario terraform makes the determination to drop the workspace attribute, and in the other scenario the determination is to keep the workspace attribute 🤔

Thanks again for the reply!

@jbardin
Copy link
Member

jbardin commented Mar 4, 2021

Yes, this is really confusing. I only briefly mentioned it to avoid complicating this further, but there also appear to be bugs with the unification process for tuples and lists. While these two types will of course have slightly different requirements for determining what the resultant type can be, in this case the different code paths are also effecting how the types they contain are being unified. This is all part of the current investigation we are doing with the hcl and cty libraries.

@apparentlymart apparentlymart added v0.14 Issues (primarily bugs) reported against v0.14 releases config labels Mar 5, 2021
@jbardin jbardin self-assigned this Mar 9, 2021
@ghost
Copy link

ghost commented Apr 16, 2021

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@ghost ghost locked as resolved and limited conversation to collaborators Apr 16, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug config confirmed a Terraform Core team member has reproduced this issue dependencies Auto-pinning explained a Terraform Core team member has described the root cause of this issue in code v0.14 Issues (primarily bugs) reported against v0.14 releases
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants