-
Notifications
You must be signed in to change notification settings - Fork 9.7k
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
lang/funcs: Add "alltrue" function #25656
Conversation
Codecov Report
|
@apparentlymart – I suppose you'd be the one who'd review a change like this? |
270c8a4
to
8b218ee
Compare
It seems like this function could have utility in Terraform. Is there any way to get it attention? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this function! This seems like a good implementation to me. I think we've seen enough questions about the need for all
and any
functions for variable validation that I think this is worth including.
I pondered a few design decisions and concluded that I agree with them:
- Choosing to accept
"true"
seems pretty much in line with the common behaviour of aggressively unifying arguments. I can't find any precedent for this specific choice withbool
, but it seems reasonable to me. - Similarly, working with all collections makes sense
- I think
all([]) == true
is the correct choice, so long as a futureany
function is defined asany([]) == false
I have one inline comment. The only other change I'd like to see is a test covering cty.TupleVal([]cty.Value{cty.True, cty.StringVal("true")})
, just for clarity. Should this function be added, I think it would be great to see any
included in a follow-up, for symmetry.
I'm also requesting a second opinion from another core team member—there may be good reasons why we shouldn't accept these functions. If you'd like to wait for the second thumbs up before responding, no problem.
Thanks again!
lang/funcs/collection_test.go
Outdated
// cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), | ||
// cty.False, | ||
// false, | ||
// }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about these commented out test cases. Why are they here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I missed your message.
{
cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}),
cty.False,
false,
},
At the time of writing, I couldn't get this to pass. It seems to pass now. ¯_(ツ)_/¯
{
cty.ListVal([]cty.Value{cty.True, cty.StringVal("true")}),
cty.True,
false,
},
When I wrote this, I didn't know ListVal requires all list element types be consistent. My intent was to ask someone on your team how to test this scenario. You offered the solution in your review: test with TupleVal.
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.False,
false,
},
This test scenario fails with the following message:
lang/funcs TestAllTrue/alltrue(cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)})) (0.00s)
TestAllTrue/alltrue(cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)})): collection_test.go:225: unexpected error: panic in function implementation: interface conversion: interface {} is *cty.unknownType, not bool
I wanted to check with you to see if this is expected. I don't really understand the usage of UnknownVal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that failure makes sense—you can't attempt to convert an unknown value to bool
.
Generally we want to propagate unknownness. If the list contains unknown values, then it is unknown if all of them are true—so we defer that decision until they become known by returning an unknown value of the correct type. In this case, that means that the return value should be cty.UnknownVal(cty.Bool)
with no error, not cty.False
.
You can use v.IsKnown()
to determine the unknownness of each element value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. The test case has been updated to look like this:
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.UnknownVal(cty.Bool),
false,
},
And the code looks like this now:
tobool := MakeToFunc(cty.Bool)
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
+ if !v.IsKnown() {
+ return cty.UnknownVal(cty.Bool), nil
+ }
got, err := tobool.Call([]cty.Value{v})
if err != nil {
return cty.False, nil
}
eq, err := stdlib.Equal(got, cty.True)
if err != nil {
return cty.NilVal, err
}
if eq.False() {
return cty.False, nil
}
}
return cty.True, nil
What is a scenario where we might run into this? During a terraform plan
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, when an element which is part of the input to the function is computed and can't be known until during apply.
🤔 I have a concern about the name function, and I'd like to hear what others think. It's not intuitive (to me) that functions named Before looking at the code (and before remembering that python has such functions), I expected that the function would take a second parameter that is the value you are looking for in the list:
I'm not necessarily opposed to the functionality, but I'm concerned by this name. For what it's worth, you can get the same result with
This returns false (which is to say, validation failed) if the list contains any |
There's a long history of names in families of functions like these.
|
You're spot on the money in the assumption that I based this implementation verbatim on how Python implements "all", and I have another branch lying around on my machine that implements the corresponding "any" function as well. I didn't implement them both in the same PR, because I didn't want to muddy the waters with implementation details of two separate functions. I think my implementation of "any" leverages Terraform's "contains" function. I'd have to double check. I agree that it would be neater if the > all([1,3,5,7,9], "<10")
false and this: > any([0,1,2,3,4,5], "== 1")
true I had thought about this some, and thought it would be really cool if we could do this, but I didn't know of a good way to implement such a thing. We don't have any functions in Terraform that take strings that are meant to be interpolated, do we? I could follow that model if we do. In the absence of such interpolated syntax, I figured we could use Terraform's list comprehension syntax to achieve something equivalent: all([for i in [1,3,5,7,9] : i < 10]) any([for i in [0,1,2,3,4,5] : i == 1]) |
It is possible in theory to do some funny tricks with function arguments where they can be evaluated in unusual ways... the However, I'm not sure how best to employ that capability here, because a callback function like this would require defining a new symbol (like the variable declarations after the Using a Perhaps we could compromise by giving them a more specific name, making it slightly less concise but also hopefully giving an extra clue as to what's going on. For example, we could call them alltrue([for i in [1,3,5,7,9] : i < 10])
anytrue([for i in [0,1,2,3,4,5] : i == 1]) What do you all think about that compromise? (This continues the existing precedent of just ramming multiple lowercase words together without any intervening underscores, which is a historical convention from early Terraform that we don't love today but that we've remained consistent with so that it's not necessary to remember which functions use underscores and which do not.) |
@apparentlymart I won't fight against (EDIT: I just read this back to myself, and it feel like I'm explaining terraform to my own lead. I'm not, just writing the full thought out, but sorry if it sounds bad). |
# What This commit adds an `all` function to Terraform configuration. A reason we might want this function is because it will enable more powerful custom variable validations. For example: ```hcl variable "amis" { type = list(object({ id = string })) validation { condition = (all([ for a in var.amis : length(a.id) > 4 && substr(a.id, 0, 4) == "ami-" ])) error_message = "The ID of at least one AMI was invalid." } } ``` If this is not the intended use of custom variable validation, the `all` function may still prove useful in other contexts where we, for example, create a resource conditionally.
8b218ee
to
fece965
Compare
The branch is rebased on master and the function name is changed from "all" to "alltrue". There is still an outstanding question from @alisdair, and a response has been given. Once the question is addressed, will be happy to make the final adjustments. Thanks, team, for discussing this! |
# What After some discussion in a code review, the thought was that "alltrue" would be clearer than "all" for the general Terraform-using populace. This commit also adds a test for verifying the "alltrue" function also works with tuples and unknown values. Good catch! 👍
fece965
to
0613a2f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updates look great, thanks for being so responsive!
Thanks! I'll follow up with the corresponding |
Yes, I'd expect so! Everything being merged now is targeting 0.14.0, the first beta is several weeks away, and I imagine the |
I coded up an implementation for Even in the example I provided earlier, this appears to be true. anytrue([for i in [0,1,2,3,4,5] : i == 1]) vs contains([0,1,2,3,4,5], 1) vs length(for i in [0,1,2,3,4,5] : i==1) > 0 I still maintain that the introduction of the Thoughts? |
Thanks for raising that, @artburkart! In the earlier discussion I got the sense that the With that framing, With that said, the trick of comparing the length of the result of a If you've already written it and the implementation is relatively straightforward then I wouldn't mind merging it since it seems like it would not represent a significant additional maintenance burden, and it seems relatively easy to describe it in the documentation. Would you mind opening a new PR for what you implemented and then we can talk about the pros and cons of introducing it in that PR? |
I've submitted a draft PR here: #26498 |
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. |
What
This commit adds an
all
function to Terraform configuration. Areason we might want this function is because it will enable more
powerful custom variable validations. For example:
If this is not the intended use of custom variable validation, the
all
function may still prove useful in other contexts where we, for example,
create a resource conditionally.