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

Type checking on objects / maps seems too tight #26164

Open
lucasvuotto opened this issue Sep 8, 2020 · 2 comments
Open

Type checking on objects / maps seems too tight #26164

lucasvuotto opened this issue Sep 8, 2020 · 2 comments
Labels
enhancement new new issue not yet triaged

Comments

@lucasvuotto
Copy link

Hello team,

Terraform Version

Terraform v0.13.0

Terraform Configuration Files

variable "x" { type = map(number) }

Debug Output

Crash Output

Does not apply.

Expected Behavior

$ echo "var.x == {}" | terraform console -var 'x={}'
true
$ echo "var.x == {a=1}" | terraform console -var 'x={a=1}'
true
$ echo "var.x == tomap({a=1})" | terraform console -var 'x={a=1}'
true

Actual Behavior

$ echo "var.x == {}" | terraform console -var 'x={}'
false
$ echo "var.x == {a=1}" | terraform console -var 'x={a=1}'
false
$ echo "var.x == tomap({a=1})" | terraform console -var 'x={a=1}'
true

Steps to Reproduce

The previous blocks are enough.

Additional Context

Does not apply.

References

Does not apply (after a quick search).


This is a excerpt from one of my modules, used for creating additional block devices for an AWS EC2 instance like extra_volumes = {"/dev/sdf" = 100}, which I consider is a legitimate use case:

variable "extra_volumes" {
  type = map(number)

  validation {
    # The `tomap` call is needed as otherwise it seems that Terraform v0.13.0
    # interprets `{"key" = 1}` as of type `object({"key" = number})`.
    # `length(var.extra_volumes) == 0` is needed as otherwise it seems that
    # Terraform v0.13.0 interprets `{}` as the empty object and doesn't seem to
    # be a way of cohersing that into `map(number)`.
    # This is all by how Terraform defines equality: same value, same type:
    condition = length(var.extra_volumes) == 0 || var.extra_volumes == tomap({
      for k, v in var.extra_volumes :
      k => v if can(regex("^/dev/sd[f-p]$", k)) && v >= 1
    })
    error_message = "Device must match /dev/sd[f-p]. Size must be positive."
  }
}

My original approach, given the lack of all or any for collections, was

variable "extra_volumes" {
  type = map(number)

  validation {
    condition = var.extra_volumes == {
      for k, v in var.extra_volumes :
      k => v if can(regex("^/dev/sd[f-p]$", k)) && v >= 1
    })
    error_message = "Device must match /dev/sd[f-p]. Size must be positive."
  }
}

I think this more readable than current approach and feels more natural.

@lucasvuotto lucasvuotto added bug new new issue not yet triaged labels Sep 8, 2020
@apparentlymart
Copy link
Contributor

Hi @lucasvuotto,

The definition of the == operator is that the two arguments must have the same type and the same value, so unfortunately this behavior is as designed. We typically recommend using == primarily with primitive-typed values and using other techniques when writing conditionals about data structures.

In this case though, as you've seen an explicit tomap conversion can work because the for expression is deriving from a map(number) value and thus all of the attributes of the object have number type, and thus tomap will unify it into a map(number) result too and the result will match.

During 0.12 development we did experiment with making == do some special automatic type inference, but the result ended up still being confusing but even harder to explain than the current rule that the types must match, and so we settled on defining == as requiring type equality and using the type conversion functions to be explicit about what types are expected in situations where they don't naturally agree.

One thing that seems to have repeatedly come up with this sort of validation block that is making an assertion about an entire collection is that there isn't a function like sum which can take an arbitrary number of boolean values and return true only if they are all true, which makes it hard to write a simple can(...) call whose result derives from a collection. If we had a function all which took an arbitrary number of bool values then this could potentially be written a different way, with the can up at the top-level:

  condition = can(all([
    for k, v in var.extra_volumes : can(regex("^/dev/sd[f-p]$", k)) && v >= 1
  ]))

This then avoids the need to use == altogether and just reduces it to a statement about what condition must be true for each of the items in var.extra_volumes.

@alisdair
Copy link
Contributor

We have a community PR for an all function here: #25656

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement new new issue not yet triaged
Projects
None yet
Development

No branches or pull requests

3 participants