hil: Conditional operator doesn't short-circuit evaluation based on result

To avoid extensive refactoring of the evaluator while implementing the conditional syntax, we accepted the limitation that it would evaluate both “sides” and then discard the inappropriate one when given syntax like this:

foo ? bar : baz

This is in most cases just a small waste of compute to evaluate a result that will never be used, but it’s troublesome in cases where one of the expressions can be evaluated without error only if its associated condition state is true. For example:

length(foo) > 0 ? foo[0] : "default"

This doesn’t work as intended because we fail on trying to index into the empty list foo in spite of the conditional.

The correct behavior is for the evaluator to evaluate the boolean expression first and then visit only one of the given expressions.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 86
  • Comments: 23 (3 by maintainers)

Commits related to this issue

Most upvoted comments

Actually, here is a wacky workaround for the second use-case:

ca_cert = "${var.ca_cert == "" ? join(" ", tls_self_signed_cert.ca.*.cert_pem) : var.ca_cert}"

workaround worked for me:

module creating instance (either 0 or 1 of them), instance id output via: output "i_id" { value = "${join("", aws_instance.i.*.id)}" } and am able to use module.bla.i_id in my output which now properly returns outs = or outs = i-blabla

length(foo) > 0 ? foo[0] : "default"

@apparentlymart: Your problem can be solved without conditional logic:

element(concat(foo, list("default"), 0)

This returns the first element of the list foo, which will be "default" if foo is empty.

More complex cases, such as @ooesili 's case can be solved using this basic pattern. His example:

resource "aws_ebs_volume" "volumes" {
  count = "${var.count}"
  snapshot_id = "${length(var.snapshot_ids) == var.count ? element(var.snapshot_ids, count.index) : ""}"
}

And the solution:

resource "aws_ebs_volume" "volumes" {
  count = "${var.count}"
  snapshot_id = "${element(concat(var.snapshot_ids, list("")), min(length(var.snapshot_ids) + 1, count.index))}"
}

With this approach, the last element of the list is an empty string. We use min() to ensure that element() will stop there rather than re-iterating through the entire list.

These solutions are entirely declarative. Arguably, they are more correct than using conditional logic.

Completely breaks patterns such as:

ca_cert = "${length(var.tectonic_ca_cert_path) > 0 ? file(var.tectonic_ca_cert_path) : ""}"

or

ca_cert = "${var.ca_cert == "" ? tls_self_signed_cert.ca.cert_pem : var.ca_cert}"

Which is not so great because it means there’s literally no way as far as I know to support generating OR importing a certificate. Except if you always create the CA resource, use heredocs to specify the existing PEM-Encoded CA cert in tfvars then use either. Hacks all over the place.

Is there a timeline for that one? It’s quite incapacitating.

Yes @MalloZup. Check hashicorp/terraform#15605.

I hit this when trying to source secrets from vault using the vault_generic_secret data source. It seems when you are using a data source like:

foo = "${var.vault_path == "" ? var.foo : data.vault_generic_secret.certs.data["foo"]}

Unlike any other resource, I can’t seem to use splat, coalesce, etc. I even attempted putting it into a null_data_source, which also proved unhelpful. Any workarounds here?