Jira (PUP-11538) Allow a Struct type to have a default key

3 views
Skip to first unread message

James Ralston (Jira)

unread,
May 6, 2022, 3:07:03 AM5/6/22
to puppe...@googlegroups.com
James Ralston created an issue
 
Puppet / New Feature PUP-11538
Allow a Struct type to have a default key
Issue Type: New Feature New Feature
Assignee: Unassigned
Components: Type System
Created: 2022/05/06 12:06 AM
Priority: Normal Normal
Reporter: James Ralston

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?

Add Comment Add Comment
 
This message was sent by Atlassian Jira (v8.20.2#820002-sha1:829506d)
Atlassian logo

James Ralston (Jira)

unread,
May 6, 2022, 3:08:01 AM5/6/22
to puppe...@googlegroups.com
James Ralston updated an issue
Change By: James Ralston
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:

{code}

class openssh::server (
  Struct[{
    'Match'          => Hash[String, Hash[String, Scalar]],
    'Subsystem'      => Hash[String, Scalar],
    default          => Scalar,
  }] $sshd_config,
) inherits openssh {
{code}


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:

{code}

class openssh::server (
  Hash[String, Variant[
    Scalar,
    Hash[String, Scalar],
    Hash[String, Hash[String, Scalar]],
  ] $sshd_config,
) inherits openssh {
{code}


But with this approach, there is nothing to stop a user from doing something like this:

{code}

openssh::server::sshd_config:
  Match: no
  Ports:
    22: yes
{code}


To defend against this, the ERB code must perform exhaustive type checking. E.g.:

{code:Ruby}

<% @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 -%>
{code}


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.:

{code}

class openssh::server (
  Hash[String, Scalar] $sshd_config,
  Hash[String, Hash[String, Scalar]] $sshd_config_match,
  Hash[String, Scalar] $sshd_config_subsystem,
) inherits openssh {
{code}

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?
Reply all
Reply to author
Forward
0 new messages