| PUP-5942 was a request to permit the Struct type to define keys using a pattern. This requiest was closed as Wontfix due to the complexity involved. I have a related but simpler (and hopefully thus viable) request: permit the Struct type to have a default key that will match any hash element, so long as the data type of the value of the key matches the specified data type. An example may be helpful in describing what I am proposing:
class openssh::server ( |
Struct[{ |
'Match' => Hash[String, Hash[String, Scalar]], |
'Subsystem' => Hash[String, Scalar], |
default => Scalar, |
}] $sshd_config, |
) inherits openssh {
|
For our in-house openssh::server module, the entire contents of the /etc/ssh/sshd_config file is passed to the module via a hash, where the key is the option name to set, and the value is the value for that option. For the vast majority of those options, the values are scalars (strings, booleans, or integers), and the ERB template code can generate the correct configuration lines in the /etc/ssh/sshd_config file without caring what each individual option is. But two sshd_config options are special and require specific data structures to pass their information to the ERB template code. The Match option takes a hash where the key is the criterion for the Match statement, and the value is a nested hash of options to set within that specific Match block. The Subsystem option takes a hash where the key is the name of the subsystem, and the value is the command for sshd to execute to invoke that subsystem. What I want to be able to enforce is that if the openssh::server::sshd_config parameter sets either the Match or Subsystem options, the values supplied conform to the data types that those options require. But I don't want to have to laboriously enumerate every one of the ~100 other sshd_config options in the Struct just in order to be able to ensure that the Match and Subsystem options conform. (Practically speaking, I cannot enumerate all of the options, even if I wanted to: different distros ship with different versions of OpenSSH, and later OpenSSH versions not only support options that earlier versions do not, but have dropped support for options that earlier versions supported.) Just as it is useful for the Puppet case statement to support a default case that matches if no other case expression matches, it would be useful for Struct to support a default schema key and data type that will match any supplied hash key not explicitly named in the Struct, so long as the data types match. Without this, the only way I can see to implement typedef checking for the Match and Subsystem options is to typedef every valid sshd_config option (which as I explained two paragraphs above is impossible), or do something like this:
class openssh::server ( |
Hash[String, Variant[ |
Scalar, |
Hash[String, Scalar], |
Hash[String, Hash[String, Scalar]], |
] $sshd_config, |
) inherits openssh {
|
But with this approach, there is nothing to stop a user from doing something like this:
openssh::server::sshd_config: |
Match: no |
Ports: |
22: yes
|
To defend against this, the ERB code must perform exhaustive type checking. E.g.:
<% @sshd_config.sort.each |option, value| do -%> |
<% if option == 'Match' -%> |
<% if value.is_a?(Hash) -%> |
<% value.sort.each |pattern, options| do -%> |
|
<% if options.is_a?(Hash) -%> |
Match <%= pattern %> |
<% options.sort.each |match_option, match_value| do -%> |
<%= match_option %> <%= match_value %> |
<% end -%> |
<% else -%> |
# <%= option %> pattern <%= pattern %> options was class <%= options.class %> but expected Hash |
<% end -%> |
<% end -%> |
<% else -%> |
# <%= option %> value was class <%= value.class %> but expected Hash |
<% end -%> |
<% elsif option == 'Subsystem' -%> |
<% ... -%> |
<% end -%>
|
This is suboptimal, to say the least. I shouldn't have to resort to performing my own typedef checking. Alternatively, I suppose I could create additional module parameters for each configuration option that requires a different typedef than a simple scalar. E.g.:
class openssh::server ( |
Hash[String, Scalar] $sshd_config, |
Hash[String, Hash[String, Scalar]] $sshd_config_match, |
Hash[String, Scalar] $sshd_config_subsystem, |
) inherits openssh {
|
But literally the only reason why I would do this would be to work around the limitation of Struct that as soon as I want to typedef any hash element the user can supply, I must predeclare and typedef every hash element the user can supply. Being able to say, "if these specific elements are in the hash their values must match these specific data types, and the values of all other hash elements that the user passed must match this specific data type" avoids this. Thoughts? |