XOR operation to describe validation rules for mutually-exclusive attributes
Current Terraform Version
Terraform v1.1.6
on linux_amd64
Use-cases
I am using the optional keyword to define an object with two properties, which are both optional, but in which at least one must be populated.
For doing so, I am using validation to enforce this condition. While it is possible, a XOR operator would make it way easier. Especially 'cause the workaround explodes in complexity when we are talking about multiple keys to check, and not just two.
Attempted Solutions
variable "example" {
type = object({
first = optional(string)
second = optional(string)
})
validation {
condition = (
# One of the following two has be true: one is set, and the other not.
(lookup(var.example, "first", null) != null && lookup(var.example, "second", null) == null) ||
(lookup(var.example, "first", null) == null && lookup(var.example, "second", null) != null)
)
error_message = "First or second must be set, but not both"
}
}
Proposal
A XOR operator, in this example, ^,
variable "example" {
type = object({
first = optional(string)
second = optional(string)
})
validation {
condition = lookup(var.example, "first", null) != null ^ lookup(var.example, "second", null) != null
error_message = "First or second must be set, but not both"
}
}
Thanks for the feature request!
I remember back when we were considering which operators to make builtins we had intentionally made the "power of" operation be a function rather than an operator both because it seems to be infrequently needed in Terraform (it's not an environment intended for mathematical work) and because the meaning of ^ is ambiguous where some other languages use it to represent power of while others use it to represent XOR.
I think the same argument holds here too: XOR seems like an operation that is occasionally useful but not frequently used, and of course the ambiguity with ^ still applies from this direction too.
Given that, I would suggest that if we do support it then we should do so using a function named xor that takes two arguments. I think we do still need to weigh whether it would be used often enough to justify it being first-class at all, but the bonus of the function formulation is that the bar it needs to meet in that regard is likely lower, because we can add new functions to the function table relatively easily without affecting any other software, whereas a new operator would need to be co-designed with various other products using the underlying HCL engine.
Incidentally, I think the lookup calls in this example aren't really doing anything because the type constraint already requires both of those arguments to be present in the object; the optional just makes Terraform fill them in as null when not specified by the caller. That means you could write var.example["first"] != null instead with equivalent effect, which hopefully allows for at least a little simplification of that complex expression with today's Terraform.
Given that, I would suggest that if we do support it then we should do so using a function named
xorthat takes two arguments.
I like this idea as well! Makes sense if it is easier to implement, and it sounds something doable.
var.example["first"] != null
Thanks for the suggestion! It definitely makes the code more readable!
Any news regarding this proposal? It would be extremely useful
I agree that adding XOR might be useful but also NOT-EQUAL operator might be used comparing BOOLEAN values. See: https://stackoverflow.com/questions/21034107/xor-operator-in-c
currently working on a validation rule where this would make things MUCH cleaner.
I have a variable of type object, with three optional but exclusive params, I'm trying to verify that ONLY one is set, so something like xor([var.foo.a, var.foo.b, var.foo.c]) would be really useful (where it just verifies only one isn't null), honestly if compact() could handle more than just string types that would work too. I tried using length(compact([var.foo.a, var.foo.b, var.foo.c])), but got the obvious, "needs a string" error.
Thanks for sharing that use-case, @aRustyDev.
I guess if we were to solve that using an xor function as we've been discussing here then it would probably look more like this in practice:
xor(var.foo.a != null, var.foo.b != null, var.foo.c != null)
XOR is an operation on boolean values, so just using var.foo.a alone would presumably require var.foo.a to be a non-null boolean value, which could be valid if var.foo were object({a = bool, b = bool, c = bool) but I assume that's not true since you mentioned nulls.
It seems technically plausible to also implement a function that takes a set of values and returns true only if exactly one of them is non-null, although it's debatable whether a more specialized function would be necessary if we had an xor function as described above. For a future reader who isn't necessarily familiar with Terraform I expect they'd be more likely to guess what the above means (based on existing knowledge of what XOR means and what != means in some other languages) than to immediately understand a Terraform-specific function that isn't commonly available in other languages.
(We are intending to implement something like #2771 in future to allow provider plugins to contribute additional functions to Terraform -- design and research for that is underway -- so at that point it'll be possible to implement more specialized functions yourself if you wish, although personally I think xor seems broad enough in its use-cases and is a familiar enough computer science concept to include that one in the set of builtins.)
A hacky alternative that does scale nicely with larger sets, is to convert the checks to numbers and sum them, then checking the total is 1. E.g.
variable "example" {
type = object({
first = optional(string)
second = optional(string)
third = optional(string)
})
validation {
condition = 1 == sum([for c in [
# add mutually exclusive values here
var.example["first"] != null,
var.example["second"] != null,
var.example["third"] != null,
] : c ? 1 : 0])
error_message = "Only one of first, second and third must be set."
}
}
or even shorter
condition = 1 == sum([for x in ["first", "second", "third"] : var.example[x] != null ? 1 : 0])
Thank you for your continued interest in this issue.
Terraform version 1.8 launches with support of provider-defined functions. It is now possible to implement your own functions! We would love to see this implemented as a provider-defined function.
Please see the provider-defined functions documentation to learn how to implement functions in your providers. If you are new to provider development, learn how to create a new provider with the Terraform Plugin Framework. If you have any questions, please visit the Terraform Plugin Development category in our official forum.
We hope this feature unblocks future function development and provides more flexibility for the Terraform community. Thank you for your continued support of Terraform!