hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io
Other
42.78k stars 9.56k forks source link

Cannot unify empty list with non-empty list when list element type is "any" #28308

Open adamdonahue opened 3 years ago

adamdonahue commented 3 years ago

I'm not sure what the issue is here, but consider this example:

Module example:

variable "example" {
  type = map(
    object({
      nested_a = object({
        nested_b = list(
          object({
            problem_variable = any
          })
        )
      })
    })
  )
  default = {}
}

resource "null_resource" "example" {
  for_each = var.example
}

Note the any.

Now, assume this top-level module:

variable "example" {
  type = map(
    object({
      name          = string
      nested_a = object({
        nested_b = list(
          object({
            problem_variable = any
          })
        )
      })
    })
  )
}

module "example" {
  source = "./example"
  example = var.example
}

Now we define a variable example, and run plan:

example = {
  "example a" = {
    nested_a = {
      nested_b = [
        {
          problem_variable = {foo = "bar"}
        }
      ]
    }
  }
}
Terraform will perform the following actions:

  # module.example.null_resource.example["example a"] will be created
  + resource "null_resource" "example" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Works fine.

Now I add a second entry to the map:

 "example b" = {
    name = "foo"
    nested_a = {
      nested_b = [
        {
          problem_variable = { foo = "baz" }
        }
      ]
    }
  }

This too works fine:

Terraform will perform the following actions:

  # module.example.null_resource.example["example a"] will be created
  + resource "null_resource" "example" {
      + id = (known after apply)
    }

  # module.example.null_resource.example["example b"] will be created
  + resource "null_resource" "example" {
      + id = (known after apply)
    }

But now let's say I have an entry, example_c, where there is no list of values:

  "example c" = {
    nested_a = {
      nested_b = []    # EMPTY.
    }
  }

I now get this error:


Error: Invalid value for module argument

  on main.tf line 17, in module "example":
  17:   example = var.example

The given value is not suitable for child module variable "example" defined at
example/main.tf:1,1-19: object is required.

So it appears it wants at least one object in the list (that matches the type of the other objects, of course).

Is this by design? Seems wrong ...

adamdonahue commented 3 years ago

This actually happens without the doubly-nested object -- maybe it even happens at the top level. I just cleaned up a real-world use case in the examples above.

adamdonahue commented 3 years ago

I just tested what happens without an any type, but rather with a specific type. In that case things work fine.

apparentlymart commented 3 years ago

Hi @adamdonahue,

Since you didn't mention which Terraform version you were using here I'm going to assume it was a v0.14 point release for now, but please let me know if that isn't true so we can make sure to try to reproduce on the same version you're using. Thanks!

adamdonahue commented 3 years ago

@apparentlymart Ugh, of course I forgot the version! We're actually on v0.13.5. We do need to upgrade. Let me see if version v0.14 has the same issue.

$ terraform version
Terraform v0.13.5

Your version of Terraform is out of date! The latest version
is 0.14.10. You can update by downloading from https://www.terraform.io/downloads.html
alisdair commented 3 years ago

Hi @adamdonahue, thanks for reporting this. I'm able to reproduce it with Terraform 0.15.0-rc2. It seems to me that an empty list always ought to be convertible to the same type as a single-item list, so I think this is a bug.

A simpler config also reproduces what I think is the same root problem:

variable "example" {
  type = map(list(any))
  default = {
    a = [ { foo = "bar" } ]
    b = []
  }
}

With Terraform 0.15, this fails with:

│ Error: Invalid default value for variable
│
│   on main.tf line 3, in variable "example":
│    3:   default = {
│    4:     a = [ { foo = "bar" } ]
│    5:     b = []
│    6:   }
│
│ This default value is not compatible with the variable's type constraint:
│ attribute types must all match for conversion to map.
╵

The any type specifier is the issue here, as you identified. As a workaround, specifying the exact type avoids this bug in Terraform's type unification process and allows it to run. For my example above, using map(list(map(string))) works fine.

adamdonahue commented 3 years ago

I was able to reproduce with a simpler example as well. I concur that this feels like a bug, as otherwise it requires at least one list item where an empty list is the right semantics for the object.

And indeed, as I noted earlier, when I moved to specific type the issue no longer occurred.

Thanks for looking into this.

Adam