terraform icon indicating copy to clipboard operation
terraform copied to clipboard

Allow generating multiple error messages when checking the validity of a collection using check assertions, preconditions/postconditions, variable validation, or test run assertions

Open davepattie opened this issue 2 years ago • 4 comments

Terraform Version

latest

Use Cases

Objects are often used with for_each logic to create multiple versions of a resource. In order to validate the variables a similar construct is required for the check block. Currently check does not support either for_each, count or dynamic so more complex interpolations are required to create a single check for multiple counts of a resource.

Attempted Solutions

locals {
  my_object = {
    one = {
      big   = 10
      small = 7
    },
    two = {
      big   = 7
      small = 10
    }
  }
  big   = 10
  small = 7
}

check "greaterthan_simple" {
  assert {
    condition     = local.big > local.small
    error_message = "big is not greater than small"
  }
}

check "greaterthan_complex" {
  assert {
    condition     = alltrue([for key, val in local.my_object : val.big > val.small])
    error_message = format("big is not greater than small for these keys: %s", join(", ", [for key, val in local.my_object : key if val.big <= val.small]))
  }
}

// Would be nice if you could do this in a for_each loop
check "greaterthan_foreach" {
  for_each = local.my_object
  assert {
    condition     = each.value.big > each.value.small
    error_message = format("big is not greater than small in key: %s", each.key)
  }
}

// Alternatively supporting dynamic would also unlock similar functionality
check "greaterthan_dynamic" {
  dynamic "assert" {
    for_each      = local.my_object
    content {
      condition       = assert.value.big > assert.value.small
      error_message = format("big is not greater than small in key: %s", assert.key)
    }
  }
}

Proposal

Add support for for_each and / or dynamic blocks to simplify the logic required to use a check block. The attempted solution gives a simple tf file and example implementations.

References

No response

davepattie avatar Nov 08 '23 15:11 davepattie

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!

crw avatar Nov 08 '23 18:11 crw

Thanks for sharing this use-case, @davepattie.

We were also chatting in another place about this and so I just wanted to note here an example of a different design I shared that I've been considering, since I too have been frustrated by writing conditions for collection-typed values where I want to treat each element as a separate entity for validation.

  assert {
    error_messages = [
      for k, v in local.my_objects :
      "Element ${format("%q", k)}: big attribute must be greater than small attribute."
      if v.value.big <= v.value.small
    ]
  }

I've shown this as an assert block to match with the examples you shared, but whatever we do here I would expect to support it in the same way for variable validation rules, preconditions, and postconditions too, since they are all intended to support similar patterns.

The way I imagine this working is that:

  • error_messages is mutually-exclusive with the condition and error_message pair that these blocks currently require. Both would be valid, but each condition block would have either a list of error messages or a single condition and associated error message.
  • The error_messages expression must return something that can convert to list(string).
  • Each element of the list becomes a separate error message, making it possible to generate a separate error message for each element of a collection rather than just a single error message which must somehow incorporate a list of all of the incorrect element keys.
  • If the list has zero elements, there are no errors, and therefore that's the same as having a condition argument that is set to true: the check passes.

Most of the syntax in my example above is not new: it's just a for expression that produces a sequence of strings. The only truly new part is the error_messages argument itself, and it would accept any valid Terraform expression that produces something that convert to a list of strings, meaning this feature could scale to more complicated situations when needed.

apparentlymart avatar Nov 08 '23 22:11 apparentlymart

Is there a way to use a check resource as a boolean for a count?

Something like this:

resource "aws_iam_service_linked_role" "AWSServiceRoleForAutoScaling" {
  count = check.role_AWSServiceRoleForAutoScaling_already_exists ? 0 : 1
  aws_service_name = "autoscaling.amazonaws.com"
  description      = "Default Service-Linked Role enables access to AWS Services and Resources used or managed by Auto Scaling."
}

check "role_AWSServiceRoleForAutoScaling_already_exists" {
  data "aws_iam_role" "AWSServiceRoleForAutoScaling" {
    name = "AWSServiceRoleForAutoScaling"
  }

  assert {
    condition = data.aws_iam_role.AWSServiceRoleForAutoScaling.name == "AWSServiceRoleForAutoScaling"
    error_message = "${data.aws_iam_role.AWSServiceRoleForAutoScaling.name} doesn't exist yet."
  }
}

Then if we could use for_each, we could be checking for multiple "conditions" which would in turn make it even easier.

scalp42 avatar Nov 16 '23 04:11 scalp42

Another example of something that would be useful for something like this:

assert {
    for_each      = [
      "serviceAccount:[email protected]",
      "user:[email protected]",
      # [...] more members here
    ]
    condition     = contains(
      keys(google_secret_manager_secret_iam_member.accessor_membership),
      each.key
    )
    error_message = "Member not found."
  }

(of course, there are probably other ways to do this, maybe using setsubtract(), alltrue() etc, but it's a little trickier, especially in the case of there being multiple objects)

wyardley avatar Sep 20 '24 16:09 wyardley