-
Notifications
You must be signed in to change notification settings - Fork 31
Parameter Types and Validation
Owner: Sam Hatfield (@sehqlr)
Date/Version: 2016-08-26
The param configuration allows users to declare values that are:
- immutable, as in, they will not be altered during converge operations
- easily referenced in other parts of your converge configuration, using the
param
keyword
The syntax aims to allow users to validate and constrain their params so that bad input is not introduced into a system managed by converge. The principle ways it does this is by allowing users to define a type of input, and additional constraints, called "rules."
The type of a parameter constrains what kind of data can be put into a param. For example, if you want a param to define how many virtual machines you need, you need that param to be an integer, not a float or your cat's name.
This is the default type for parameters.
Functions available in rules and predicates:
Name | Description | Rule Example |
---|---|---|
len |
Counts number of chars in the string, returns an int | `len |
empty |
this is a shorthand for `len | eq 0` |
oneOf |
checks for value's membership in a list of other strings | oneOf apple blackberry cherry |
notIn |
this is a shorthand for not oneOf
|
notIn lions tigers bears |
This is the first type added to parameters. Like string
, this type can be
inferred from the default value.
Functions available in rules and predicates:
Name | Description | Rule Example |
---|---|---|
min |
is the same as param.Value >= arg
|
min 45 |
max |
is the same as param.Value <= arg
|
max 8 |
range |
this is shorthand for and (min x) (max y)
|
range 4 12 |
isEven |
is the same as param.Value % 2 == 0
|
`len |
isOdd |
this is shorthand for not isEven
|
`len |
A "rule" is a more specific constraint for a parameter. They are represented as a partial text/template expression, with the value of the parameter piped in.
Params are configured in an HCL block, with the following fields: default, type, and either rule or rules.
param "<name>" {
# all of these fields are optional
default = <value>,
type = <string|int>,
<rule|rules|ruleset> = <definition>,
}
Any or none of these:
Name | Input | Description | Notes |
---|---|---|---|
default | any plaintext | the value of the param, if not overridden by other config | If this is not included, then the user must provide a value elsewhere |
type |
string or int
|
what kind of values can be assigned to this param | if this is not included, then it's inferred from default , or falls back on string
|
Only one of these:
Name | Input | Description | Notes |
---|---|---|---|
rule | nested HCL block | a single test to validate the parameter | the syntax for the rule block is described below |
rules | an array of predicates | rule blocks condensed into a shorthand form | Each string is parsed as a must rule with a standard error message |
ruleset | name of a ruleset block | a ruleset is modularized collection of rules | rulesets are defined in their own block, see below |
param "<name>"
...
rule {
<level> = "<predicate>"
err = "<message>"
}
The err
field is required, and one, and only one, of the options for
<level>
must be present. The field used defines the behavior of
converge when a param does not validate.
Options for <level>
:
Name | Description |
---|---|
should | if validation fails, exit with return code 1 |
must | if validation fails, exit with return code 2 |
param "goldilock's password" {
type = int
rules = [
"len | range 8 12",
"ne password",
]
}
The argument passed to the ruleset is named by the <argument>
field. You can refer to that value in the predicates with $argument
.
param.ruleset "<name>" "<argument>" {
type = "<string|int>"
add "<name>" {
description = "<description>"
<level> = "<predicate>"
err = "<message>"
}
add "<name>" {
...
}
...
}
These snippets will validate:
param "name" { }
# type is string
param "quorum" {
type = int
rule {
should = "isOdd"
err = "use an odd number of nodes for fault tolerance"
}
rule {
must = "min 1"
err = "you have to have at least one control node!"
}
}
param "blocksize" {
default = 128,
# type is inferred from default
rules = [
"range 50 512",
]
}
param "password" {
# type defaults to string
rules = [
"len | range 5 25",
]
}
param "cipher" {
rules = [
"oneOf Rijndael Serpent Twofish",
"notIn DES Blowfish",
]
}
Along with params that don't pass validation, these snippets will fail with additional errors:
# this throws an error:
# "float32 is an unsupported type.
# Use one of these instead:
# {{list of supported types}}"
param "unsupported_type" {
type = float32,
rules = [
"isEven",
"range 1 34",
]
}
# this throws an error:
# "isPrime is an unsupported extension for type int.
# Use one of these instead:
# {{list of supported extensions}}"
param "unsupported_extension" {
default = 7,
type = int,
rule {
must = "isPrime",
err = "primes are magical"
}
}
The expression is injected into a validation template, which is then rendered with the value of the param. If the template renders a truthy string and no errors, then the param is valid. Otherwise, it is invalid, and an error is returned with the text from err. The validation template checks the type of the value first automatically. Validate takes a list of strings and injects them into this template:
{{ . | $expression }}
Because of how values are piped between expressions in go templates, function calls in expressions take the provided value as their last argument. (aside: we should have an internal function for rearranging these for other funcs.) Each validation returns an error value. If a validation returns nil, then it is valid.
This is what the syntax will look like in future versions. Most of the work will go to adding new types and extensions for them.
Type | Inferred? | What it represents |
---|---|---|
decimal | yes | floating point numbers and percentages |
url | no | URLs, including protocall, but not including GET args |
ipv4 | no | IPv4 addresses |
ipv6 | no | IPv6 addresses |
CIDR | no | CIDR subnet notation |
blocksize | no | storage capacity as number + unit (8MiB) or % of disk space |
These are valid:
param "dns_server" {
default = 8.8.8.8
type = ipv4
# validation is implicit
}
param "vm_size" {
default = 128MiB,
type = blocksize,
rules = [
"range 50MiB 512MiB",
]
}
The same invalidations apply from the Demo. The list of valid types and extensions for those types would be added to the list output.
This is an example that assumes that there is some way to load in third-party modules
param.ruleset "gpg_key" {
# this assumes some loading/registering process for 3rd party code
type = custom.GpgKey
# $param is the value passed into the ruleset
rule "passphrase_protected" {
must = "custom.isPassphraseProtected $param"
msg = "gpg key is not passphrase protected"
}
rule "is_signed_by_Bob" {
must = "custom.isSignedBy $param {{param `fingerprint.bob`}}"
msg = "Bob did not sign this key!"
}
}
param "public_key" {
ruleset = "gpg_key {{file `key`}}"
}
Users defining their own validators and types will have to write those error messages. However, all of their messages will be prepended with "The module loaded at line X did this..."