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

XOR operation to describe validation rules for mutually-exclusive attributes #30571

Open
rpadovani opened this issue Feb 22, 2022 · 11 comments
Open
Labels
enhancement functions new new issue not yet triaged

Comments

@rpadovani
Copy link

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"
  }
}
@rpadovani rpadovani added enhancement new new issue not yet triaged labels Feb 22, 2022
@crw
Copy link
Collaborator

crw commented Feb 23, 2022

Thanks for this feature request! If you are viewing this issue and would like to indicate your interest, please use the 👍 reaction on the issue description to upvote this issue. We also welcome additional use case descriptions. Thanks again!

@apparentlymart
Copy link
Contributor

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.

@apparentlymart
Copy link
Contributor

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.

@rpadovani
Copy link
Author

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 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!

@apparentlymart apparentlymart changed the title Implement XOR operator XOR operation to describe validation rules for mutually-exclusive attributes Sep 16, 2022
@obvionaoe
Copy link

Any news regarding this proposal?
It would be extremely useful

@vadym
Copy link

vadym commented Sep 25, 2023

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

@aRustyDev
Copy link

aRustyDev commented Sep 26, 2023

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.

@apparentlymart
Copy link
Contributor

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.)

@DuncanvR
Copy link

DuncanvR commented Dec 1, 2023

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])

@crw
Copy link
Collaborator

crw commented Mar 7, 2024

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!

@lorenzofelletti
Copy link

I have always felt that Terraform lacks for some sort of xor operator. I also think that a onetrue(list) or exactlyonetrue(list) function would do the trick and be a very valuable built-in function to have, while being more explicit than xor(list) or a new ^ operator.

It is a functionality that is so often needed in validation rules that I feel having to rely on an external provider to implement it, or hack your way around it every time is very cumbersome, while the code to add it would be very simple.

(I'd be happy to implement it, in case you agree that it would be a valuable addition to Terraform.)

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

No branches or pull requests

9 participants