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

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

Ability to raise an error #15469

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

Comments

@timgurney-ee
Copy link

@timgurney-ee timgurney-ee commented Jul 4, 2017

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
Member

@apparentlymart apparentlymart commented Jul 5, 2017

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

@timgurney-ee timgurney-ee commented Jul 5, 2017

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

@timgurney-ee timgurney-ee commented Nov 10, 2017

Nice work around!

@fishpen0
Copy link

@fishpen0 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 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 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 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

@smiller171 smiller171 commented Apr 30, 2019

@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

@Vince-Chenal Vince-Chenal commented Jul 2, 2019

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

@smiller171 smiller171 commented Jul 2, 2019

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

@smiller171 smiller171 commented Jul 2, 2019

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

@bwoznicki bwoznicki commented Jul 8, 2019

@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

@gordonbondon gordonbondon commented Jul 9, 2019

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

@smiller171 smiller171 commented Jul 9, 2019

@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

@bwoznicki bwoznicki commented Jul 9, 2019

@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

@smiller171 smiller171 commented Jul 10, 2019

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

@queglay
Copy link

@queglay 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 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.

@thundertaker
Copy link

@thundertaker thundertaker commented Oct 25, 2019

I also would like to use some kind of native resource for assertions, especially when passing generated values to Terraform modules.

@tomasbackman
Copy link

@tomasbackman tomasbackman commented Dec 27, 2019

The simple variation that @dmrzzz gave above is still working.
But it is a little bit sad that there are still not some better/native way to do asserts and similar.
It would be very useful when writing reusable modules that can be run with different user provided input...

@apparentlymart maybe a first step could be to just have an "assert-inputs" function for var. values like you suggested earlier.
The more complex cases when certain resources and modules need to be validated and to find it out and fail early in planning already would also be very nice, but one small step at a time, right =)

@queglay
Copy link

@queglay queglay commented Dec 27, 2019

i've started to find that using a local-exec with simply

echo "Failed Message" >&2
exit 1

is working well for me.

....extended to ansible in local-exec I'm doing this now-

resource "null_resource" "fail-test" {
  provisioner "local-exec" {
    command = <<EOT
      exit_test () {
        RED='\033[0;31m' # Red Text
        GREEN='\033[0;32m' # Green Text
        BLUE='\033[0;34m' # Blue Text
        NC='\033[0m' # No Color
        if [ $? -eq 0 ]; then
          printf "\n $GREEN Playbook Succeeded $NC \n"
        else
          printf "\n $RED Failed Playbook $NC \n" >&2
          exit 1
        fi
      }
      ansible-playbook -i "$TF_VAR_inventory" ansible/fail-test.yaml; exit_test
  
EOT

  }
}
@smiller171
Copy link

@smiller171 smiller171 commented Jan 3, 2020

@queglay The problem is this won't fail until apply time. Ideally we should be able to fail the plan

@queglay
Copy link

@queglay 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 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

@adamday2 adamday2 commented Feb 12, 2020

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

@aaronsteers aaronsteers commented Mar 4, 2020

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

@syedrakib syedrakib 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.

@smiller171
Copy link

@smiller171 smiller171 commented Jun 3, 2020

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

@aaronsteers
Copy link

@aaronsteers aaronsteers commented Jun 3, 2020

@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

@aaronsteers aaronsteers commented Jun 3, 2020

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 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 aaronsteers commented Jun 17, 2020

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

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

  • Please vote via thumbs up and watch the raise(err_msg) PR (#25088) if you want to see this functionality added to Terraform in a future release.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.