Puppet's optional parameters might not be what you think they are
Photo by Kevin Ku: https://www.pexels.com/photo/data-codes-through-eyeglasses-577585/
Recently I encountered a puppet-lint warning regarding one of the classes I maintain at a customer. And this warning lead me down a rabbit hole of things I knew, things I understood and things I didn't know and apparently haven't grasp fully before. Surprisingly I never encountered this warning before. Despite being, most likely, a pretty common one.
So, I learned a bit or two about "optional parameters" in Puppet and thought this will give a decent read for my blog.
The problem
Take the following class definition. The code/logic is omitted as it doesn't matter (likewise I removed the puppet-strings documentation at the start of the manifests). We can purely focus on the class definition.
class profile::services::test (
String $blatest,
String $password = "test",
Pattern[/^\d+\.\d+\.\d+(?:-\d+)?$/] $version = '11.2.3-4',
String $system = 'SOMEEXAMPELSTRING',
Optional[Hash[Stdlib::Port, Stdlib::Host]] $backupservers,
) {
# intentionally left empty
}
puppet-lint printed out the following warning:
WARNING: optional parameter listed before required parameter on line 6 (check: parameter_order)
And I was stumped by it. "Line 6 IS an optional parameter!" I thought. "There is no parameter defined after this, so how can it be listed before a required parameter?" I was confused. I moved line 6 around and sometimes the warning would vanished, sometimes not. Always defying what I thought I knew was correct.
The documentation is always right?
When you pass a certain amount of time you should stop to fiddle around and start solving it in an iterative way. So I went on to read the documentation. Thinking that maybe I missed some change in the Puppet DSL.
That's when I came across the paragraph on "Display order of parameters" as it mentioned the term "optional parameter". However.. It defined it in a totally different way I was used to use this term.
Display order of parameters
In parameterized class and defined resource type definitions, you can list required parameters before optional parameters (that is, parameters with defaults). Required parameters are parameters that are not set to anything, includingundef
. For example, parameters such as passwords or IP addresses might not have reasonable default values.
You can also group related parameters, order them alphabetically, or in the order you encounter them in the code. How you order parameters is personal preference.Note that treating a parameter like a namevar and defaulting it to
$title
or$name
does not make it a required parameter. It should still be listed following the order recommended here.Source: The Puppet language style guide: Display order of parameters
And here I learned that: Any parameter with an associated default-value is called an optional parameter. But what about the Optional-data type?
Puppet goes on to give us one good and one bad example regarding the ordering:
Good:
class dhcp (
$dnsdomain,
$nameservers,
$default_lease_time = 3600,
$max_lease_time = 86400,
) {}
Bad:
class ntp (
$options = "iburst",
$servers,
$multicast = false,
) {}
This recommendation is the reason why the puppetlabs-puppet-lint Gem enforces the order of: Required parameters first, optional parameters last. Where "optional parameter" again means: Any parameter with an associated default-value.
I changed line 6 by setting an empty hash as the default-value and the puppet-lint warning was gone. I also could have moved line 6 above or below line 2 (String $blatest,
). Which would have fixed the warning too.
But here it is more or less the coding-style to list Optional-data type parameters after all others. Regardless of being a required or optional parameter. And again, this is being done here this way as people watch them as "optional parameters" in the sense of "I don't have to use if I don't need them". And here the requirement for parameter ordering bites the official definition of "optional parameters"...
The solution:
class profile::services::test (
String $blatest,
String $password = "test",
Pattern[/^\d+\.\d+\.\d+(?:-\d+)?$/] $version = '11.2.3-4',
String $system = 'SOMEEXAMPELSTRING',
Optional[Hash[Stdlib::Port, Stdlib::Host]] $backupservers = {},
) {
# intentionally left empty
}
A bit more explanation
Now that we have clarified what an optional parameter is: You are either already confused, or ask yourself where the confusion originates from. So, without further ado, let me introduce you to the Optional data type.
The Optional data type
TheOptional
data type wraps one other data type, and results in a data type that matches anything that type would match plusundef
. This is useful for matching values that are allowed to be absent. It takes one required parameter.Source: Puppert: Values, data types, and aliases: The Optional data type
Most likely you have seen that parameters in Puppet classes are defined with an identifier of the type of data they will store. Same as it's required in any programming language where you have to define type of the variable (Integer, Float, Boolean, String, etc.). The only difference to Puppet is that in Puppet this is not enforced (as to avoid the term "optional" here. 😅)
If we adopt the given "good example" from above, we can make it look like this:
class dhcp (
String $dnsdomain,
Hash[String] $nameservers,
Optional[Integer] $default_lease_time = 3600,
Integer $max_lease_time = 86400,
) {}
Here $default_lease_time
is an optional parameter, right? Yes, but not because it uses the Optional data type. It's only an optional parameter because it has a default-value of 3600 assigned. Likewise $max_lease_time
is an optional parameter too.
And here the problem originates.
Most of people I encountered use the term "optional parameter" to refer to the $default_lease_time
parameter and not $max_lease_time
. Which is absolutely understandable given how your speech works, how we like to name things and how we combine those two.
Adding to this is the fact that parameters which are initialized with the additional Optional-data type are allowed to be undef
additionally to the other types of data defined. Effectively making it optional to assign a value to them. And this is exactly how these type of parameters are often used. Hence the term "optional parameter" sounds logical when referring to these parameters.
Technically however, that's wrong. Alas in my case it was the reason why I spent more time than I liked fixing that one linter warning. But I learned something along that way. I think that's what matters, right?
A little bit more confusion?
When reading through all data language types definitions and data types I found a data type I knowingly never encountered before. The Undef and NotUndef data types.
The Undef data type
The data type of
undef
isUndef
. It matches only the valueundef
, and takes no parameters.Several abstract data types can match the
undef
value:
- The
Data
type matchesundef
in addition to several other data types.- The
Any
type matches any value, includingundef
.- The
Optional
type wraps one other data type, and returns a type that matchesundef
in addition to that type.- The
Variant
type can accept theUndef
type as a parameter, which makes the resulting data type matchundef
.- The
NotUndef
type matches any value exceptundef
.Source: Puppert: Values, data types, and aliases: The Undef data type
The NotUndef data type
TheNotUndef
type matches any value exceptundef
. It can also wrap one other data type, resulting in a type that matches anything the original type would match exceptundef
. It accepts one optional parameter.Source: Puppert: Values, data types, and aliases: The NotUndef data type
And yes, they are useful for what they are designed. But I REALLY wished Puppet would rename that Optional data-type to something else. (Is: CanBeUndef
still available? 😂)
Conclusion
All definition problems aside: This shows again why the usage of proper development/analyze tools like Linters is so important. It not only helps reducing errors, no. It forces you to understand what you are doing. 😉