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

contains function documentation does not clearly define how it decides whether a value is present #35414

Open
rifelpet opened this issue Jul 2, 2024 · 5 comments

Comments

@rifelpet
Copy link

rifelpet commented Jul 2, 2024

Terraform Version

Terraform v1.9.0
on darwin_arm64

Terraform Configuration Files

variable "foo" {
  type = list(string)
  default = ["a", "b"]

  validation {
    condition = contains([
      ["a", "b"],
      ["b", "a"],
    ], var.foo)
    error_message = "may only contain both 'a' and 'b' strings"
  }
}

Debug Output

https://gist.github.com/rifelpet/387ed4c86bbf8809c19bc4c411ecdf54

Expected Behavior

The validation should have passed and the plan succeeded

Actual Behavior

The validation failed

Steps to Reproduce

terraform plan

Additional Context

The condition should evaluate to true, similar to the behavior in terraform console:

> contains([["a", "b"], ["b", "a"]], ["a", "b"])
true

but for some reason the validation condition is evaluating to false and triggering the validation failure. The same issue occurs if setting the variable to an explicit (valid) value via tfvars or similar.

Simplifying the variable type to just a string works as expected:

variable "foo" {
  type = string
  default = "a"

  validation {
    condition = contains([
      "a",
      "b",
    ], var.foo)
    error_message = "may be set to either 'a' or 'b'"
  }
}

References

No response

@rifelpet rifelpet added bug new new issue not yet triaged labels Jul 2, 2024
@apparentlymart
Copy link
Member

Hi @rifelpet! Thanks for reporting this.

Your input variable is defined as having type list(string), so a more accurate simulation of this rule in terraform console would be the following:

> contains([["a", "b"], ["b", "a"]], tolist(["a", "b"]))
false

var.foo has already been converted to list(string) (if necessary) by the time the validation rule is evaluated, whereas your first argument to contains is a tuple containing two values of type tuple([string, string)]. Only values of the same type can compare equal, so your validation rule always returns false.

You could make this particular case work by making the first argument to contains be a tuple of lists instead of a tuple of tuples:

  validation {
    condition = contains([
      tolist(["a", "b"]),
      tolist(["b", "a"]),
    ], var.foo)
    error_message = "may only contain both 'a' and 'b' strings"
  }

...though I assume this was just a contrived example for easier reproduction, so applying this solution to your real configuration may require some different details. The general idea, though, is to make sure you are always comparing list(string) values to other list(string) values, and using tolist is a good general way to force Terraform to reinterpret a tuple as a list.

@apparentlymart
Copy link
Member

apparentlymart commented Jul 2, 2024

A different way to interpret this rule is to consider this variable to be a set(string) rather than list(string), since it seems to have the usual characteristics of a set: ordering is not relevant, and duplicate values are not meaningful. With that interpretation there's a more direct way to describe this rule as a set operation:

variable "foo" {
  type    = set(string)
  default = ["a", "b"]

  validation {
    condition     = length(setsubtract(var.foo, ["a", "b"])) == 0
    error_message = "may only contain both 'a' and 'b' strings"
  }
}

setsubtract knows that its arguments are supposed to be sets, so no explicit type conversion is needed with this framing. The rule can be read as "after removing these two elements (if present), the set is empty".

I hope that one of these two variations is applicable enough to your real configuration that you can apply it. Terraform seems to have behaved as intended -- the rule that two values must be of the same type to compare equal is fundamental, and contains is behaving consistently with that here -- and so I think the solution is to change your expression to correctly describe the rule you are intending to enforce in terms of Terraform's existing language features rather than to change the Terraform language to make your example work. (That would be a breaking change in any event, so cannot be done under the Terraform v1.x Compatibility Promises).

Please let me know if you try one of these changes and it doesn't work, or if the solutions I've described don't seem as applicable to your real configuration as they were to the contrived reproduction case. Thanks!

@apparentlymart apparentlymart added the waiting-response An issue/pull request is waiting for a response from the community label Jul 2, 2024
@rifelpet
Copy link
Author

rifelpet commented Jul 3, 2024

Hi @apparentlymart

Thanks for the quick response. I understand the issue now, that ["a", "b"] is considered a tuple rather than list. As a long time user of Terraform this behavior is surprising and I would expect them to be interchangeable in situations like this, though I realize making such a change now would be considered breaking.

The docs in both types and type-constraints do not mention the difference between lists and tuples, they're only ever used interchangeably.

It does mention:

  • A list can only be converted to a tuple if it has exactly the required number of elements.

In this case, I would expect contains([["a", "b"], ["b", "a"]], tolist(["a", "b"])) to return true because the list type in the second argument can be converted (back) to a tuple to match the element types in the first argument's list. Why is that not the case?

In either case, wrapping the contains() list elements with tolist() will work for my real configuration so I'll proceed with that.

Thanks again!

@apparentlymart
Copy link
Member

The contains function is essentially the same as visiting every element of the list or tuple given in the first argument and testing whether it equals (with the same meaning as ==) the second argument.

Automatic type conversion rules only come into play when the context allows Terraform to infer your intent. For example, the + operator is defined only for numbers and so Terraform can safely assume that if you are using that operator then you probably intended to interpret the arguments as numbers, and so it will attempt to convert them. Likewise the setunion function is defined as working only with sets and so it can safely automatically convert its arguments to sets when possible.

More general operators and functions like the == operator and the contains function can make no such assumptions, because they are defined to work with values of any type. Therefore you must perform any necessary conversions explicitly yourself in those cases.

The requirement for both operands to be of the same type is documented as part of the documentation on the == and != operators since that's where it's most relevant. However, I can see that there's no explicit mention of it in the contains documentation: it's defined very informally in a way that is intended to imply that it's performing an equality test, but it would be clearer for it to say so directly and then link to the other documentation that describes how equality works.

Therefore I think we should resolve this issue by changing the contains function to describe its behavior more explicitly in terms of the equality operator. Perhaps something like this:

contains determines whether the list, tuple, or set given in its first argument contains at least one element that is equal to the value in the second argument, using the same definition of equality as the == operator described in Equality Operators.

I'm going to label this as a documentation issue so that the relevant team can find it more easily. Thanks!

@apparentlymart apparentlymart added documentation and removed bug waiting-response An issue/pull request is waiting for a response from the community new new issue not yet triaged labels Jul 3, 2024
@rifelpet
Copy link
Author

rifelpet commented Jul 3, 2024

That sounds good to me. Feel free to re-title the issue too.

@apparentlymart apparentlymart changed the title Variable Validation incorrectly fails with contains() and complex value contains function documentation does not clearly define how it decides whether a value is present Jul 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants