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

Value assertions / validations in configuration files #2847

Closed
thegedge opened this issue Jul 25, 2015 · 30 comments
Closed

Value assertions / validations in configuration files #2847

thegedge opened this issue Jul 25, 2015 · 30 comments

Comments

@thegedge
Copy link
Contributor

It would be nice to assert conditions on values, extending the schema validation idea to the actual config language. This could probably be limited to variables, but even standalone assertion statements could be a possibility (e.g., maybe we need to validate multiple variables in tandem).

As an example, suppose I have a variable that I want to be one of three values – e.g., test, staging, or production – and maybe my stack / module won't fail if given another value, but I want to enforce it to be one of the three. It would be nice to specify these requirements somewhere. Here's one possibility:

variable "environment" {
  default = "test"
  description = "Stack environment"
  require = "${oneof("test", "staging", "production")}"
}

Where the require attribute must evaluate to a true value or terraform would raise an error. Another option, instead of more interpolation functions, would be to include a few basic require_xyz attributes:

variable "environment" {
  default = "test"
  description = "Stack environment"
  require_oneof = ["test", "staging", "production"]
}

variable "enable_foo" {
  default = true
  require_type = "boolean"
}

variable "cidr_block" {
  require_regex = "\d{,3}.\d{,3}.\d{,3}.\d{,3}/[0-3][0-9]"
}

And so on. The benefit of the latter option is that you can return nicer error messages whereas the former provides interpolation functions that may be useful even beyond this context.

In some cases these values will end up in attributes for resources, at which point the attribute's validation may spit back an error, but if you're considering modules nested inside modules nested inside modules, it's nice to know at exactly which point the invalid input occurred. In other words, it's great that module.foo.module.bar.module.spam.aws_vpc.cidr_block is invalid, but perhaps the CIDR block input was constructed at module.bar from input given by module.foo.

@radeksimko
Copy link
Member

This might help at least with some very basic validation:
#4795 (should be part of the next release)

@feniix
Copy link

feniix commented May 4, 2017

require_regex and require_oneof would be an awesome feature :)

@ahl
Copy link
Contributor

ahl commented Oct 5, 2017

Any further thoughts on this? I'd love to see something like this. Any suggested workarounds?

@ahl
Copy link
Contributor

ahl commented Oct 6, 2017

I solved our problem with this; does this do what you want?

https://github.com/ahl/terrassert

@Jamie-BitFlight
Copy link

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

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
}

@alexjurkiewicz
Copy link

Here's another use case. I'd like to ensure that my AWS ALB health check interval is greater than my AWS ALB health check timeout.

@paddie
Copy link

paddie commented Feb 13, 2018

We have certain conventions we like to make sure are met when creating resources around this organisation using terraform.

A simple example is that specific AWS resources need to be lower-case. In other cases, we might want to make sure that a specific variable to a module exists in a static list of valid values etc.

I was thinking that either the keywords assert, guard would work, but require would also work:

variable "input" {
  type = "list"
  default = true
  # ensure that input values are distinct
  assert = "${distinct(var.input) ==  var.input}"
}

I'm not super sold on using var.input but it could also be a way of actually make it more versatile instead of making up some new lambda syntax or something. That would of course mean that we load all variables before validating.

We are specifically interested in this feature to prevent situations which have appeared in the past where resources have simply been named erroneously or following outdated conventions that we'd like to catch.

References

@amrit3327
Copy link

@apparentlymart have we got the enhancement to put constraints to the input in terraform such as require_one_of ? or it still work in progress

@ruke47
Copy link

ruke47 commented Mar 7, 2019

I use terraform to configure AWS Auto-scaling Groups, and would love to be able to put a constraint on my module that desired_capacity <= max_size; I frequently merge commits that pass terraform plan only to see them fail on apply because I've left my max_size variable at its default value.

@thegedge
Copy link
Contributor Author

thegedge commented Mar 8, 2019

@ruke47 perhaps not as desirable, but you could make use of signum to likely create an invalid value. I'm thinking something like:

signum(max_size  - desired_capacity + 1) * desired_capacity

wherever you need to use desired_capacity. I'm thinking in most contexts that will result in a failing plan, unless the attribute doesn't consider negative numbers invalid. Not ideal, but could be a stopgap.

@ghost
Copy link

ghost commented May 23, 2019

I was using the workaround provided by @Jamie-BitFlight above, but this no longer works with 0.12. You get an error that the argument is no expected.
I was using this to ensure that when performing a blue/green deployment, the user isn't trying to disable the resources that are active to customers. This greatly increases my need for some form of validation or assertion.

@andyprinceuk
Copy link

I'm currently using a pattern like this with 0.12:

resource "null_resource" "check_no_a_or_b" {
  count = contains(["a", "b"], "a") ? "a" : 0  # Error: There is an "a" or "b"
}

The output is not 100% explicit

Error: Incorrect value type

  on main.tf line 1, in resource "null_resource" "check_no_a_or_b":
 1:   count = contains(["a", "b"], "a") ? "a" : 0  # Error: There is an "a" or "b"

Invalid expression value: a number is required.

but, it does achieve a validation check, and the # Error: <comment> is reflected in the output

@gtirloni
Copy link

We also need a proper solution to ensure module users are supplying valid values. These hacks are nice but something officially supported would be better.

@RobCannon
Copy link

It would be great if there could be multiple assertions, along with a user friendly error message. Something like:

variable "input" {
  type = "string"
  description = "Very bad password validation"
  assertions = [
    assert {
      evaluate = lower(var.input) == var.input
      message = "var.input must be lower case"
    },
    assert {
      evaluate = length(var.input) >= 8
      message = "var.input must be at least 8 characters"
    }
  ]
}

@kiernan
Copy link

kiernan commented Sep 23, 2019

Some of the workarounds above were for older versions or didn't quite work. So here's an example that has worked for me recently to get enum behaviour:

variable "environment" {
  type = string
}
locals {
  environment_options = ["dev", "stg", "prod"]
}
resource "null_resource" "is_environment_valid" {
  count = contains(local.environment_options, var.environment) ? 0 : "invalid" # ERROR: Invalid value provided
}

@scott1138
Copy link
Contributor

scott1138 commented Sep 28, 2019

I ran across the change in tf .12 and @teamterraform had a great solution (I think) in #22901 using a local map and key lookup. The same principal can be used with a list and index with a few less key strokes with the same effect. If you name the local testing the value well enough it should be clear what the issue it.

locals {
  valid_envs = ["Dev","Test","Prod"]
  validate_env = index(local.valid_envs,var.env)
}

When a bad value is provided:

on tf12gen.tf line 5, in locals:
   5:   validate_env = index(local.valid_envs,var.env)
    |----------------
    | local.valid_envs is tuple with 3 elements
    | var.env is "tst"

@davehewy
Copy link

davehewy commented Oct 7, 2019

It is strange to me that this has been being asked for since 2015 and yet input validation still remains absent from Terraform. There also doesn't seem to be much of an explanation officially for why? Happy to be corrected.

@jamiekt
Copy link

jamiekt commented Oct 28, 2019

I'd like to add my use case from #23205 which was closed as it "seems to be covering the same problem or request" as the one here.


We use terraform to deploy resources to GCP and we use labels upon those resources to enable fine-grained analysis of billing. For example, we have a label with a key of component and we encourage each product team within our organisation to create that label with the same value on all their resources - thus we can aggregate all expenditure per component.

The set of values that we expect to be used for that component label is finite and short but GCP does not provide a way to restrict the actual value that gets supplied. This creates two problems:

  • Inevitably some developers forget to create this label
  • If they do create it then they use a non-allowed value, thus we have a detritus of values showing up in our custom billing reports

We provide terraform modules for our various teams to use to deploy GCP resources. Those terraform modules allow us to control how resources get configured/deployed and thus allow us to mandate a component label by providing a module variable var.component for which a value must be supplied - that gets us around the problem developers forgetting to create the label. However, we still don't have a way to restrict the value that gets supplied, thus we still get the detritus of values.

My suggestion is that terraform variables can optionally be configured with a specified with a set of allowed values. Something like this perhaps:

variable "component" {
  type    = "string"
  default = "core"
  allowed_values = ["core", "componentA", "componentB", "componentC"]
}

@oqf
Copy link

oqf commented Oct 31, 2019

I ran across the change in tf .12 and @teamterraform had a great solution (I think) in #22901 using a local map and key lookup. The same principal can be used with a list and index with a few less key strokes with the same effect. If you name the local testing the value well enough it should be clear what the issue it.

locals {
  valid_envs = ["Dev","Test","Prod"]
  validate_env = index(local.valid_envs,var.env)
}

When a bad value is provided:

on tf12gen.tf line 5, in locals:
   5:   validate_env = index(local.valid_envs,var.env)
    |----------------
    | local.valid_envs is tuple with 3 elements
    | var.env is "tst"

This is the approach I am currently using.

@jamiekt
Copy link

jamiekt commented Nov 1, 2019

@oqf this is brilliant, thank you. Time to expedite our move to terraform 12.

@apparentlymart
Copy link
Contributor

Hi all,

In the forthcoming Terraform 0.12.20 we're planning to introduce an experimental new feature intended to meet the major use-cases discussed in this issue. The initial design is to add a new block type validation which can appear inside variable blocks, like this:

variable "environment" {
  type = string

  validation {
    condition     = contains(["test", "staging", "production"], var.environment)
    error_message = "Argument \"environment\" must be either \"test\", \"staging\", or \"production\"."
  }
}

Each variable block can have zero or more validation blocks, and all of the custom validations for a variable must succeed for a given value to be considered valid.

Terraform will evaluate the expression given in condition with the given variable value available for referencing. In the above example, the validation for variable "environment" refers to var.environment. For this initial design, the condition may only refer to the variable being validated, because at this time it's technically infeasible for the condition to refer to other objects in the module. This particular design constraint is very unlikely to change for this first iteration of the feature, but unplanned future work may change the technical constraints to make it more feasible in a later incarnation.

If the condition expression returns true then the value is considered valid. If condition returns false then the value is invalid and Terraform will produce an error message that includes the sentence(s) given in the error_message argument, which should be written to blend nicely with the parts of the message generated by Terraform itself:

Error: Invalid value for variable

  on example.tf line 5, in module "example":
   5:   environment = "toast"

Argument "environment" must be either "test", "staging", or "production".

This was checked by the validation rule at modules/example/variables.tf:15,3-13.

A condition expression should never produce an error itself, because that would mask the caller's own error. However, often in Terraform the success or failure of some expression is the most concise way to express a validation condition, so we've added a new function can intended specifically for using the success of an expression as a boolean value in condition:

variable "cidr_block" {
  type = string

  validation {
    condition     = can(regex("^\d{,3}.\d{,3}.\d{,3}.\d{,3}/[0-3][0-9]$", var.environment))
    error_message = "Argument \"cidr_block\" must be an IPv4 CIDR prefix in the standard CIDR prefix notation."
  }
}

The function regex fails with an error if it can't find at least one match in the given string, but we can call it in an argument to the can function to turn that failure into a false result as condition expects. This then allows a more concise condition expression than if we were forced to carefully avoid the error case.


This feature is currently experimental and subject to breaking changes with no automatic upgrade path even in patch releases of Terraform. We do not recommend using this feature in "production" modules at this stage, and to avoid accidentally depending on the experimental feature we're currently requiring an explicit opt-in using a new argument in a terraform block in each module where you will use the feature:

terraform {
  experiments = [variable_validation]
}

This opt-in marker will be required as long as the feature remains experimental. Activating this experiment will cause Terraform to emit a warning to make sure all users of the module are aware that it is depending on an experimental feature that may break in future Terraform releases.

We plan to introduce the variable_validation experiment in Terraform 0.12.20. There is not yet specific plan for when this feature might leave experimental status, because that will depend on the volume and nature of feedback.

Alternatively, the implementation for this is already in the master branch in preparation for the release, so if you have a Go development environment and want to get a head start you could choose to build it yourself.


If you do have feedback on this experimental feature once it's available, please open a new feature request issue and complete the template to show what you're trying to do and what challenges you ran into, if any.

This issue already has a long and varied discussion history, so please avoid leaving feedback comments directly in reply to this comment so we can keep the notification noise to a minimum for those who are watching this issue for development updates. (If this announcement leads to a lot of chatter in this issue then I may temporarily lock it for a while just to manage notification noise, but 🤞 that won't be necessary.)

We're excited to hear you feedback. Thanks!

@AndiDog
Copy link
Contributor

AndiDog commented Jan 11, 2020

@apparentlymart can the experiment also run an external script for validation of a variable? Or check things like "file exists"?

@MarkKharitonov
Copy link

One thing I noticed about this experimental feature - you cannot reference other variables in the condition.

E.g., I want to make sure a certain variable is actually given when another variable has a certain value. Something like this:

variable "tfstate_sa_name" {
    type = string
    description = "The name of the storage account to store the remote backend state of this component. Required when role is component"
    default = ""

    validation {
        condition     = var.tfstate_sa_name != "" || var.role != "component"
        error_message = "The \"tfstate_sa_name\" variable must be given when configuring the remote backend for regular components."
    }
}

Not allowed:

Error: Invalid reference in variable validation

  on ..\..\modules\remote_state\main.tf line 27, in variable "tfstate_sa_name":
  27:         condition     = var.tfstate_sa_name != "" || var.role != "component"

The condition for variable "tfstate_sa_name" can only refer to the variable
itself, using var.tfstate_sa_name.

@apparentlymart
Copy link
Contributor

Hi @MarkKharitonov,

Indeed, that's an intentional limitation at least for this initial version of the feature because it avoids a situation where a variable could potentially have additional dependency edges apart from what comes from its expression in the calling module, and while that might be possible someday it would've made this initial implementation far more risky and complicated.

Would you mind opening a new feature request issue describing that use-case? That way when we close out this issue (once the new feature stops being experimental) we won't lose track of this enhancement request.


As I noted in my comment above, that guidance applies to all feedback on the experiment: please open new issues, rather than leaving comments here, because that way we can make sure that we see all of the feedback and can track the outcome of each response separately, with regard to whether it's been resolved in the initial release or whether it's deferred as a later enhancement, and also we can have multiple threads of discussion rather than trying to discuss everything all at once in here. Thanks!

@aloksahoo
Copy link

I had situation to restrict deployments from the default workspace using v0.12.20(not using the experimental feature)
non_default_workspace_chk = index(terraform.workspace == "default" ? []:[terraform.workspace], terraform.workspace)

when a default workspace is used then the below error is thrown,

` non_default_workspace_chk = index(terraform.workspace == "default" ? []:[terraform.workspace], terraform.workspace)
|----------------
| terraform.workspace is "default"

Call to function "index" failed: cannot search an empty list.`

@avarmaavarma
Copy link

Hi all,

In the forthcoming Terraform 0.12.20 we're planning to introduce an experimental new feature intended to meet the major use-cases discussed in this issue. The initial design is to add a new block type validation which can appear inside variable blocks, like this:

variable "environment" {
  type = string

  validation {
    condition     = contains(["test", "staging", "production"], var.environment)
    error_message = "Argument \"environment\" must be either \"test\", \"staging\", or \"production\"."
  }
}

Each variable block can have zero or more validation blocks, and all of the custom validations for a variable must succeed for a given value to be considered valid.

Terraform will evaluate the expression given in condition with the given variable value available for referencing. In the above example, the validation for variable "environment" refers to var.environment. For this initial design, the condition may only refer to the variable being validated, because at this time it's technically infeasible for the condition to refer to other objects in the module. This particular design constraint is very unlikely to change for this first iteration of the feature, but unplanned future work may change the technical constraints to make it more feasible in a later incarnation.

If the condition expression returns true then the value is considered valid. If condition returns false then the value is invalid and Terraform will produce an error message that includes the sentence(s) given in the error_message argument, which should be written to blend nicely with the parts of the message generated by Terraform itself:

Error: Invalid value for variable

  on example.tf line 5, in module "example":
   5:   environment = "toast"

Argument "environment" must be either "test", "staging", or "production".

This was checked by the validation rule at modules/example/variables.tf:15,3-13.

A condition expression should never produce an error itself, because that would mask the caller's own error. However, often in Terraform the success or failure of some expression is the most concise way to express a validation condition, so we've added a new function can intended specifically for using the success of an expression as a boolean value in condition:

variable "cidr_block" {
  type = string

  validation {
    condition     = can(regex("^\d{,3}.\d{,3}.\d{,3}.\d{,3}/[0-3][0-9]$", var.environment))
    error_message = "Argument \"cidr_block\" must be an IPv4 CIDR prefix in the standard CIDR prefix notation."
  }
}

The function regex fails with an error if it can't find at least one match in the given string, but we can call it in an argument to the can function to turn that failure into a false result as condition expects. This then allows a more concise condition expression than if we were forced to carefully avoid the error case.

This feature is currently experimental and subject to breaking changes with no automatic upgrade path even in patch releases of Terraform. We do not recommend using this feature in "production" modules at this stage, and to avoid accidentally depending on the experimental feature we're currently requiring an explicit opt-in using a new argument in a terraform block in each module where you will use the feature:

terraform {
  experiments = [variable_validation]
}

This opt-in marker will be required as long as the feature remains experimental. Activating this experiment will cause Terraform to emit a warning to make sure all users of the module are aware that it is depending on an experimental feature that may break in future Terraform releases.

We plan to introduce the variable_validation experiment in Terraform 0.12.20. There is not yet specific plan for when this feature might leave experimental status, because that will depend on the volume and nature of feedback.

Alternatively, the implementation for this is already in the master branch in preparation for the release, so if you have a Go development environment and want to get a head start you could choose to build it yourself.

If you do have feedback on this experimental feature once it's available, please open a new feature request issue and complete the template to show what you're trying to do and what challenges you ran into, if any.

This issue already has a long and varied discussion history, so please avoid leaving feedback comments directly in reply to this comment so we can keep the notification noise to a minimum for those who are watching this issue for development updates. (If this announcement leads to a lot of chatter in this issue then I may temporarily lock it for a while just to manage notification noise, but 🤞 that won't be necessary.)

We're excited to hear you feedback. Thanks!

I am trying out this validation block - and it works well as long as you have a single string input variable. How do you accommodate a list(string) - say I need a list of values of validated?

@apparentlymart
Copy link
Contributor

@avarmaavarma it sounds like you've been trying out the experimental feature and have run into something unclear about it. If so, it would be helpful if you would open a new issue using the "Feature Request" template to show more about what you tried to do and what happened when you tried it. Then I may be able to help you use the feature to solve your problem, but it will also be useful to help improve the feature and/or the documentation about it before it is released as stable. Thanks!

@avarmaavarma
Copy link

avarmaavarma commented Feb 26, 2020

Here is the feature request - #24223

Essentially, input validation (love the experimental feature - much needed!), does not allow a collection of values to be validated.

And yes - a workaround until then, would be very valuable. I am curious as to how folks are currently validating sets of input values...

Thanks

@achew22
Copy link

achew22 commented May 20, 2020

Is there any plan to add something like a testcase to the validation? It would be nice to have some inline examples of good and bad values for ensuring complex validation behaves as expected.

Thanks!

@apparentlymart
Copy link
Contributor

Hi all,

The custom variable validation feature will become stable (non-experimental) in the forthcoming 0.13 release. Because all of the feedback we saw based on the experiment was functionality that could be added later without breaking compatibility, we decided to leave the feature exactly as originally implemented for the initial release and then improve it gradually in subsequent releases.

Terraform 0.13's beta testing period will start imminently, where you can try this out if you wish. With that said, aside from the experiment opt-in no longer being required the functionality has not changed at all, so if you already tried it out under the experiment you may wish to just wait until 0.13.0 final to use the stable version.

Because the change is already merged in the master branch ready for release, I'm going to close this. As before, if you have any feedback or ideas for this feature that are not already captured as separate GitHub issues, please open either an enhancement or bug report issue (as appropriate) so we can track each of the discussions separately.

Thanks!

@hashicorp hashicorp locked as resolved and limited conversation to collaborators Jun 1, 2020
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