Since the arguments you're populating are defined as taking CIDR block addresses, in principle the provider could validate these, but it seems like it doesn't have its own validation rules in this particular case and so it's relying on the remote API's validation that will therefore take effect only during the apply step.
It may be worth opening an issue in the provider's GitHub repository to discuss the possibility of adding built-in validation here, but for the sake of this answer I'll assume that's impossible and discuss how you can define validation rules in your own configuration instead.
With these values hard-coded directly inside the resource configuration, there is no recourse for custom validation because the contents of a resource
block are (aside from some special arguments like count
and for_each
) entirely the responsibility of the corresponding provider.
However, if you are intending to write a shared Terraform module that takes these IP addresses as input variables and then refers to them from the resource configuration, you can write custom validation rules to describe additional constraints that must be true in order for a particular value of that variable to be considered valid.
The condition
argument in a validation
block expects a boolean value which is true
if the condition holds and false
if the condition is not met. However, since Terraform also has lots of built-in functions that are defined to fail with an error if their own validation rules aren't met, Terraform also has a special can
function which evaluates an experssion in a special context where a failure is translated to a false
result, and any success is translated to a true
result, thus meeting the expectations of the condition
argument.
A typical strategy then is to identify an existing Terraform function which has the same validation rule that you want to apply and to call that value inside can
to use its success or failure as the validation condition.
For CIDR block addresses in particular, Terraform has a small number of functions which take those as input and produce derived CIDR block addresses or IP addresses. For example, cidrnetmask
takes an IPv4 CIDR block address and returns the same address in netmask notation. It will fail if the given string isn't a valid IPv4 CIDR block, and so it seems like a good candidate for describing the rule you want to check here:
variable "source_address" {
type = string
validation {
condition = can(cidrnetmask(var.source_address))
error_message = "Must be a valid IPv4 CIDR block address."
}
}
Your example here has an extra requirement of accepting multiple addresses, and so that requires a slightly more complicated validation rule to test whether the condition holds for all members of the given collection, which is what the alltrue
function is intended for:
variable "source_addresses" {
type = set(string)
validation {
condition = alltrue([
for a in var.source_addresses : can(cidrnetmask(a))
])
error_message = "All elements must be valid IPv4 CIDR block addresses."
}
}
The [ for ... ]
expression here is producing a list of boolean results from trying can(cidrnetmask(a))
for each element a
of the set. alltrue
then interprets that result and returns a single true
only if every element of the list is true
.
In order for these rules to apply, you'll need to pass the potentially-invalid IP addresses through these input variables, instead of providing them directly inside the resource
block as in your example. That only really makes sense if you are intending to write a reusable module where callers will be providing different IP addresses for each instance. If you intend to have just a fixed set of IP addresses anyway, it'd be simpler to just make sure they are valid when you initially specify them and trust that they'll therefore stay valid in future unless changed.