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

Ability to raise an error #15469

Closed
timgurney-ee opened this issue Jul 4, 2017 · 44 comments
Closed

Ability to raise an error #15469

timgurney-ee opened this issue Jul 4, 2017 · 44 comments

Comments

@timgurney-ee
Copy link

One feature I have not been able to find (or code around) is the lack of ability to raise an intentional error.

There are times where I need certain things to be correct for example 2 lists to be of the same size, or a variable to be one of a certain set of values etc.

So the addition of a raise_error function or similar would be useful. I was thinking of it being an additional interpolation function something that works like this:

raise_error(condition, message)

for example

raise_error(${length(var.array1) == length(var.array2)}, 'The arrays need to be the same length')

if the condition fails, then the message is displayed otherwise the processing continues.

@apparentlymart
Copy link
Contributor

Hi @timgurney-ee! Thanks for this suggestion.

It sounds like what you're looking to do here is to guarantee certain invariants that your configuration depends on and prevent Terraform from trying to process a configuration if those invariants don't hold.

I've thought before about the idea of having a way to make "assertions" in the Terraform config that get tested before Terraform will take any further action. For example (made-up syntax here just for illustration purposes):

require "matching_arrays" {
  test = "${length(var.array1) == length(var.array2)}"
  message = "array1 and array2 must be the same length"
}

I'd imagined this working by creating a new node in the graph representing the requirement, and then visiting that node during all graph walks. If the test expression returns <computed> (because it refers to a resource attribute we can't know yet) then we'd proceed optimistically assuming the assertion is true, but if it returns a concrete false we'd fail.

In the above example where only var. interpolations are used, this would be able to fail early in the "validate" walk, which would be the most ideal behavior.

The following case is trickier:

require "distinct_foo_and_baz" {
  test = "${aws_instance.foo.id != aws_instance.baz.id}"
  message = "instances foo and baz must be distinct"
}

This is a contrived example, but it illustrates a case where we'd be unable to return the error until after both instances are created in the apply step. Fortunately I think most real-world uses-cases of this would apply to variables and data sources, and would thus generally be taken care of during either the "validate" or the "plan" step.

Another part of this would be defining which resources should only be processed if the invariant is satisfied. This could be achieved by a new special node type in depends_on, like this:

resource "aws_instance" "bar" {
  # ... (normal attributes) ...

  depends_on = [
    "require.matching_arrays",
    "require.distinct_foo_and_baz",
  ]
}

This would then let Terraform know that it mustn't try to real with this instance until after the requirement has been checked. Without this, Terraform's normal concurrent processing of resources could allow the instance to get processed before the assertion is processed, in the event that the assertion is being processed at apply time due to referring to other computed resource attributes.

This is all just a sketch for now. Not sure what this would actually look like, but I'm curious to hear if you think the above would help solve the problem you're trying to solve. I think a first-class block would work better for this than an interpolation function because it gives us this ability to control the processing order via normal dependencies, which would be much harder with an interpolation function.

@timgurney-ee
Copy link
Author

I think you have definitely hit on what I was trying to do with the suggestion.

For the use case I was thinking of it would normally be known vars however the extended examples with interpolated results also make a lot of sense but also highlight the complexity of the problem.

The overall descriptions would solve the problem I am looking at, assert is effectively what I am thinking of. I didn't take my thoughts as deep or as detailed you, as I was only considering a smaller subset of issues.

I would be happy if this came in in stages, maying stage one just being pre-defined vars with values, and then maybe extending it to handle interpolation at a later stage?

@Jamie-BitFlight
Copy link

Jamie-BitFlight commented Nov 10, 2017

Hey guys,

I found a way today how you can hack in asserts into the current version of Terraform.
I have banged out this example https://www.linkedin.com/pulse/devops-how-do-assertion-test-terraform-template-jamie-nelson/

TL;DR

variable "environment_list" {
  description = "Environment ID"
  type = "list"
  default = ["dev", "qa", "prod"]
}
variable "env" {
description = "Which environment do you want (options: dev, qa, prod):"
}
resource "null_resource" "is_environment_name_valid" {
  count = "${contains(var.environment_list, var.env) == true ? 0 : 1}"
  "ERROR: The env value can only be: dev, qa or prod" = true
}

or to use your test:

resource "null_resource" "is_array_length_correct" {
  count = "${length(var.array1) == length(var.array2) ? 0 : 1}"
  "array1 and array2 must be the same length" = true
}

@timgurney-ee
Copy link
Author

Nice work around!

@fishpen0
Copy link

fishpen0 commented Dec 6, 2017

Based on discussion in #16848, it would be valuable for it to be possible to raise errors even when terraform is run using -target. The current workaround and proposed solution does not make this possible.

If the require method had an additional flag for always_run or something similar, this would cover additional use cases

@louis-gounot
Copy link

louis-gounot commented May 29, 2018

Why not using a data source from a provider instead ?

An assert provider with an assert_equals data source for exemple (can be extended later if required) ?

We can still create explicit dependencies if necessary.

Main advantage I see is that most data sources can be checked during the refresh phase for sanity checks (not resource dependent). It also allows to reuse existing terraform logic without adding new grammar to the language.

@nandac
Copy link

nandac commented Nov 20, 2018

Does this hack still work with terraform v0.11.10. I keep getting the invalid key error every single time when I run terraform plan. Is this the expected behavior?

@ervinb
Copy link

ervinb commented Dec 6, 2018

@nandac It doesn't work for me either. I've got around it by conditionally executing a failing command with local-exec.

resource "null_resource" "dns_check" {
  count = "${data.external.check_dns_setup.result.valid == true ? 0 : 1}"

  provisioner "local-exec" {
    command     = "false"
    interpreter = ["bash", "-c"]
  }
}

@smiller171
Copy link

@nandac @ervinb This workaround is working for me on v0.11.13. If the count is 0 it doesn't evaluate the null_resource when planning.

@Vince-Chenal
Copy link

Hey everyone,

I was using @Jamie-BitFlight's workaround but it does not work anymore on Terraform 0.12.
The goal of his solution was to make Terraform fail (by evaluating an invalid key) in the cases we are interested in.
The reason why it does not work anymore is because Terraform evaluates the invalid key even when the count is 0.

I also tried the "local-exec" workaround but it's only executed when applying which is too late for me.

I found a similar approach that works with Terraform 0.12.
The idea is to initialize the "triggers" parameter of the null_resource with a valid map when your assertion is respected and with something else when you need to make it fail.

I first tried with an empty string:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : ""
}

But Terraform want the ternary to be consistent with types. i got this error:

The true and false result expressions must have consistent types. The given
expressions are object and string, respectively.

Then i tried with the "file" function and it worked:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : file("ERROR: your assertion is not ok")
}

The output is a bit weird due to the fact it tries to open a file, but it works ...
Call to function "file" failed: no file exists at ERROR: your assertion is not

After that i noticed that using triggers will generate a change each time you apply.
The last thing i added is a lifecycle rule to ignore changes on triggers.

The final version of the workaround:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : file("your assertion is not ok")
    lifecycle {
        ignore_changes = [
            triggers
        ]
    }
}

@smiller171
Copy link

May be useful to folks here: I've created a custom provider specifically for triggering failures on plan: https://github.com/rhythmictech/terraform-provider-errorcheck

example:

####code:

locals {
  compare     = "success"
  testSuccess = "success"
  testFail    = "fail"
}

resource "errorcheck_is_valid" "shouldMatch" {
  name = "shouldMatch"
  test = local.compare == local.testSuccess
}

resource "errorcheck_is_valid" "Not_valid_if_not_match" {
  name = "Not_valid_if_not_match"
  test = local.compare == local.testFail
}

output:

terraform validate .

Error: Not Valid

  on main.tf line 11, in resource "errorcheck_is_valid" "Not_valid_if_not_match":
  11: resource "errorcheck_is_valid" "Not_valid_if_not_match" {

@smiller171
Copy link

At some point I'd like to look at setting up a custom error message, but it's already much cleaner and more future-proof than hacking around the HCL parser

@bwoznicki
Copy link

@smiller171 looks like we had the same idea :) however I went with data_source as there is no need to save any values in state.
https://github.com/bwoznicki/terraform-provider-assert

It would be nice to have assert-like functionality in terraform as default especially now with all the additional functions in terraform 0.12x

@gordonbondon
Copy link

Some time ago I tried to solve it with similar data source approach, but inside a module https://github.com/gordonbondon/terraform-common-verify

@smiller171
Copy link

@bwoznicki @gordonbondon The problem with doing it in a data source instead of a provider is that it won't error until you try to apply. Doing it in a provider lets you throw an error on plan or validate

@bwoznicki
Copy link

@smiller171 data source is validated early look at this: https://www.terraform.io/docs/configuration/data-sources.html#data-source-lifecycle and https://www.terraform.io/docs/extend/writing-custom-providers.html#data-sources unless you are targeting a resource output that is know after apply it will fail at the plan stage. Run an example of workspace test in https://github.com/bwoznicki/terraform-provider-assert and you will see that it fails regardless of how many resources you have there.

@smiller171
Copy link

@bwoznicki interesting. I went with a provider specifically because I thought data sources weren't evaluated during planning.

@queglay
Copy link

queglay commented Jul 14, 2019

I am also struggling with this problem during ansible provisioning inside local_exec. if there are ansible errors, I'd like them to be able to stop terraform from continuing provisioning to make it easier to find out what went wrong (otherwise the log is massive, and no colors doesn't make it easy). Is it not possible to raise an error to terraform and stop it from continuing provisioning?

@dmrzzz
Copy link

dmrzzz commented Jul 25, 2019

Thanks @Vince-Chenal for the new workaround.

AFAICT the following simpler variation seems to also work with Terraform 0.12.5:

locals {
  assert_not_default_workspace = terraform.workspace == "default" ? file("ERROR: default workspace not allowed") : null
}
$ terraform-0.12.5 validate

Error: Error in function call

  on main.tf line 2, in locals:
   2:   assert_not_default_workspace = terraform.workspace == "default" ? file("ERROR: default workspace not allowed") : null

Call to function "file" failed: no file exists at ERROR: default workspace not
allowed.

Honestly if we just had an error() interpolation function (as OP suggested) to use instead of file(), I'd be pretty happy with this.

@queglay
Copy link

queglay commented Jan 5, 2020

Apologies, I see the value in that too. I was stumped on how to ensure ansible playbooks that failed on apply would stop the rollout from continuing.

@aware74
Copy link

aware74 commented Feb 12, 2020

This seems like a pretty important feature to have. Are there any thoughts on providing this within Terraform itself in a way that is both compatible with 0.12.x and that doesn't require opening a non-existent file?

I'll definitely take a look at https://github.com/bwoznicki/terraform-provider-assert as well as its fork https://github.com/rvodden/terraform-provider-assert

@adamday2
Copy link

While this may not completely solve the issue, just wanted to add this reference for anyone looking at the ticket:

https://www.terraform.io/docs/configuration/variables.html#custom-validation-rules

@ximon18
Copy link

ximon18 commented Feb 25, 2020

Another workaround is to use the regex function as "If the given pattern does not match at all, the regex raises an error.". One downside of this approach is that the error message tells you what pattern was not matched but doesn't tell you why the pattern is not permitted.

@gchamon
Copy link

gchamon commented Feb 27, 2020

@adamday2 it works for basic assertion, but you cannot cross reference other variables. If you must provide either variable a or b, you can't check in a if b has also been provided.

I am just pointing this out for anyone reading this thread, as you said it yourself that it doesn't solve the issue.

@aaronsteers
Copy link

I've added a feature proposal in #24269 which (in combination with the newly added try() and can()) should at least in theory address all the core requirements discussed here.

The core idea in my proposal is that we can keep this very simple: raise(error_message) and just leverage the native if-then-else constructs in combination with the newly added (and extremely helpful!) try() wrapper. Importantly, this doesn't modify the core HCL syntax at all. It's very similar to the modulal approach of @gordonbondon, except that it can operate in a locals block or inline anywhere that expressions are accepted.

Please raise a vote there and join the discussion on #24269 if you think this would be a valuable addition. Thanks!

@syedrakib
Copy link

I am actually more in favour of having raise accept just an error message (like proposed in #24269) as opposed to raise accepting both a condition and an error message. IMO the condition should be determined outside the scope of the raise. Keeps the code-readability simpler and easier.

@smiller171
Copy link

@aaronsteers @syedrakib Do try() and can() get evaluated during plan, or only during apply?

@aaronsteers
Copy link

@smiller171 - My understanding is that try() and can() apply when possible, during plan if sufficient information is available or during the apply otherwise.

@aaronsteers
Copy link

I have created a new PR to add the raise(error_msg) function, which hits the HCL and Terraform github repos: #25088

@aaronsteers
Copy link

aaronsteers commented Jun 3, 2020

I am actually more in favour of having raise accept just an error message (like proposed in #24269) as opposed to raise accepting both a condition and an error message. IMO the condition should be determined outside the scope of the raise. Keeps the code-readability simpler and easier.

@syedrakib - The new PR #25088 attempts to provide this. It still needs a bunch more testing but functionality-wise, I think it's mostly there.

If anyone has Golang experience and can help continue to testing/review, this would be very much appreciated!

@aaronsteers
Copy link

aaronsteers commented Jun 17, 2020

May be resolved by: #25088 (PR waiting feedback, CI tests passing)

@timgurney-ee (OP) and subscribers to this issue:

@paboum
Copy link

paboum commented Mar 22, 2021

Protip: instead of inserting 1/0 somewhere in the code to cause a runtime error, one needs to actually try using the +Inf which it yields in some sophisticated manner:

$ terraform console
> (1/0)
+Inf
> (1/0)*0

>
Error: Operation failed

  on <console-input> line 1:
  (source code not available)

Error during operation: can't multiply zero by infinity.

> (1/0)+1
+Inf
> (1/0)-(1/0)

>
Error: Operation failed

  on <console-input> line 1:
  (source code not available)

Error during operation: can't subtract infinity from itself.
> (1/0)/(1/0)

>
Error: Operation failed

  on <console-input> line 1:
  (source code not available)

Error during operation: can't divide zero by zero or infinity by infinity.

Hopefully, whoever implemented it, won't add paradoxes other than Hilbert's Hotel.

@joeyciechanowicz
Copy link

While waiting for the raise PR, you can simply specify an invalid type for a count to cause an error. it's not such a great error message but it works!

resource "null_resource" "is_array_length_correct" {
  count = length(var.array1) == length(var.array2) ? 0 : "Array lengths do not match"
}

/// outputs
Error: Incorrect value type

  on ecs-service/main.tf line 9, in resource "null_resource" "is_array_length_correct":
   9:   count = length(var.array1) == length(var.array2) ? 0 : "Array lengths do not match"
   
Invalid expression value: a number is required.

@smiller171
Copy link

@joeyciechanowicz the errorcheck module I wrote is cleaner https://registry.terraform.io/modules/rhythmictech/errorcheck/terraform/latest

@joeyciechanowicz
Copy link

@joeyciechanowicz the errorcheck module I wrote is cleaner https://registry.terraform.io/modules/rhythmictech/errorcheck/terraform/latest

It absolutely is, it's great. However it does requires adding a 3rd party dependency, whereas the null provider is maintained by Hashicorp.

@smiller171
Copy link

Fair enough, though I'd argue my 15 lines of Python are simple enough to audit.

@korinne
Copy link

korinne commented Feb 16, 2022

Hi to those following this thread 👋 The Terraform Core team is currently researching this issue, and we'd love to get more feedback from those interested in this functionality. If you'd like to participate in the research, you can comment on this Discuss topic. It'd be great to setup a call and get your use-cases -- thanks so much advance!

@julianxhokaxhiu
Copy link

For anyone crossing this issue, Terraform is going to have something like this implemented on 1.2.0. See https://discuss.hashicorp.com/t/update-ability-to-raise-an-error/37732

@alisdair
Copy link
Contributor

alisdair commented Apr 6, 2022

Terraform 1.2 will include a language change which aims to address the use cases described in this issue: preconditions and postconditions for resources, data sources, and outputs. This is now available for testing in the first Terraform 1.2.0 alpha release. Draft documentation is also available.

If you're tracking this issue, I'd encourage you to try out the alpha release. Feedback is welcome on the Terraform discussion forum, and if you encounter bugs please file a GitHub issue. Thanks!

@alisdair alisdair closed this as completed Apr 6, 2022
@hashicorp hashicorp locked as resolved and limited conversation to collaborators Apr 6, 2022
@apparentlymart
Copy link
Contributor

A small addition to @alisdair's previous note:

At the time of writing this the most recent alpha release v1.2.0-alpha-20220328 still has the feature behind an opt-in-experiment guard, so you'll need to enable it to use the features described in the draft document:

terraform {
  experiments = [preconditions_postconditions]
}

The current implementation on the main branch has it no longer marked as experimental and so any subsequent alpha releases, and eventually the beta release, will not require this opt-in. However, note that although the design is relatively finalized now we may still make some minor changes to it before it's included in a final release.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests