hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.41k stars 9.5k forks source link

New (configuration language) string function - isnullorempty #32720

Open BrianRichardson opened 1 year ago

BrianRichardson commented 1 year ago

Terraform Version

1.3.9

Use Cases

In the interests of clean concise configuration code, it would be most useful to have functions for evaulating whether string values are null, empty or blank. Another very similar function would be "isnullorwhitespace".

isnullorempty: Indicates whether the specified string is null or an empty string (""). isnullorwhitespace: Indicates whether a specified string is null, empty, or consists only of white-space characters.

Without these functions it is necessary to evaulate numerous conditions, e.g. isNullOrEmpty = var.value == null || var.value == "" isNullOrWhiteSpace = var.value == null || var.value == "" || var.value = " "

Attempted Solutions

isNullOrEmpty = var.value == null || var.value == "" isNullOrWhiteSpace = var.value == null || var.value == "" || var.value = " "

Proposal

isNullOrEmpty = isnullorempty(var.value) isNullOrWhiteSpace = isnullorwhitespace(var.value)

References

No response

apparentlymart commented 1 year ago

Hi @BrianRichardson! Thanks for sharing this use-case.

Can you share more about what situations you have where you would find it useful to treat both null and the empty string as similar?

There are some legacy cases where Terraform providers (and one specific function: coalesce) do that for backward compatibility with configurations written for Terraform 0.11, which didn't have null. But for modern Terraform we typically expect to use null as the sole representation of "not set", without any special treatment of any other values as synonyms for it.

Is your concern one of backward compatibility, or do you have a different motivation?

Thanks!

BrianRichardson commented 1 year ago

Hi @apparentlymart

I am not concerned with backward compatibility.

There are occasions where string values, which could be configured either as variables or properties of an object, and then passed through or manipulated through locals. Potentially a string value started out as a non-null value of length greater than zero characters, was then manipulated through locals and potentially resulted in a string with zero characters or just whitespace. In this case, I would wish to add a check or conditional to ensure the value is genuinely set and useful, rather than allowing the blank value to propagate to my resource. In some cases, I would choose to replace the blank value with a default one instead.

Other programming languages, e.g. C#, have similar functions.

apparentlymart commented 1 year ago

Hi @BrianRichardson,

From what you've said here it seems like the zero-length strings are more a mistake than an intentional way to express "this is unset" or "use a default". If that is true then I would suggest that either input variable validation or resource preconditions (depending on the context) might be a good way to express that.

Here's an example of an input variable which is allowed to be null or a non-empty string but an empty string is invalid:

variable "example" {
  type     = string
  nullable = true

  validation {
    condition     = var.example != ""
    error_message = "An empty string is not a valid value."
  }
}

Here's a variation with a default value instead of the null, but where the empty string is still invalid:

variable "example" {
  type     = string
  nullable = false
  default  = "example"

  validation {
    condition     = var.example != ""
    error_message = "An empty string is not a valid value."
  }
}

Here's a similar example using a precondition on a resource which itself requires a non-empty string:

resource "example" "example" {
  something_id = var.something_id

  lifecycle {
    precondition {
      condition = var.something_id != ""
      error_message = "The something ID may not be an empty string."
    }
  }
}

I would not typically suggest just silently treating an empty string (or any other value) as unset because that breaks the usual idiom that null means unset, and does not give good feedback when someone is using your module incorrectly.

However, if you do want to provide a default value as a fallback when a value is either null or an empty string then you could potentially rely on the legacy behavior of coalesce that I mentioned earlier, which for backward compatibility reasons does violate this rule of not treating the empty string as unset:

locals {
  example = coalesce(var.example, "default")
}

With the above expression, local.example would be "default" if var.example were either null or "", but would otherwise have the same value as var.example.

Would any of these approaches suit your situation?

BrianRichardson commented 1 year ago

Hi @apparentlymart

I dont disagree with you, this is probably accurate.

I have tried using validation and preconditions, but sadly, find they fall short. I find with these I need to repeat identical logic due to lacking basic capabilities such as being able to reference variables and that defining custom functions does not seem to be properly supported.

crw commented 6 months ago

Thank you for your continued interest in this issue.

Terraform version 1.8 launches with support of provider-defined functions. It is now possible to implement your own functions! We would love to see this implemented as a provider-defined function.

Please see the provider-defined functions documentation to learn how to implement functions in your providers. If you are new to provider development, learn how to create a new provider with the Terraform Plugin Framework. If you have any questions, please visit the Terraform Plugin Development category in our official forum.

We hope this feature unblocks future function development and provides more flexibility for the Terraform community. Thank you for your continued support of Terraform!

kbcz1989 commented 6 months ago
can(coalesce(trim(var.something, " ")))

locals.tf:

locals {
  test_vars = {
    null_var                       = null
    empty_string_var               = ""
    whitespace_string_var          = " "
    multiple_whitespace_string_var = "  "
    string_var                     = "test"
  }
}

terraform console:

> { for k, v in local.test_vars : k => can(coalesce(trim(v, " "))) }
{
  "empty_string_var" = false
  "multiple_whitespace_string_var" = false
  "null_var" = false
  "string_var" = true
  "whitespace_string_var" = false
}