Alexander
Alexander

Reputation: 11

puppet-rspec - How to pass parameters to nested profile classes during unit test?

I am trying to build two Puppet profiles for the Hashicorp Consul DCS. Consul can run as a client or server agent, the server mode being a superset of the client mode. This is directly mirrored in the configuration:

Consul server agents typically require a superset of configuration required by Consul client agents.

My Puppet design approach is based on this pattern: https://puppet.com/docs/pe/2018.1/the_roles_and_profiles_method.html

According to the Puppet documentation, it should be possible (and most probably desirable) to include the consul_client profile in the consul_server profile to avoid code duplication:

Profiles can include other profiles.

Trying to implement this, I used some mandatory parameters on both profiles and ran into problems during execution of the automatic rspec unit tests.

In the consul_client unit test file consul_client_spec.rb, I just provided the required parameters as follows:

let(:params) { {
  'datacenter' => 'unit-test',
  'encrypt' => 'DUMMY',
  'server_agent_nodes' => [ '1.2.3.4' ]
} }

Issues arised when trying to run the consul_server_spec.rb unit test. Naively, I just passed the one additional required parameter of the consul_server profile:

let(:params) { {
  'bootstrap_expect' => 3,
} }

As the consul_client profile is includeed / requireed by the consul_server profile, the test failed with missing parameters for the consul_client profile class. This seems to be indicative of some general structural problem with this approach.


Now, I am unsure if I should re-declare all the parameters of the consul_client profile class in the consul_server profile class - which, in my opinion, would violate the DRY principle. Also, when using Hiera data in the future, this would lead to a situation where profile::consul_client::* and profile::consul_server::* would contain some of the same, duplicate data, as the client-related part of the data would have to be repeated for both profiles.

Added Note: And duplicating parameters in the consul_server class would probably not even work, as parameters cannot be passed explicitly, but only via data, to include-like resource definitions - so those duplicated parameters couldn't be passed to the consul_client class.

On the contrary, the documentation states the following, but I am not sure if this applies to included profile classes (as they may not be component classes?) as well:

Profiles own all the class parameters for their component classes. If the profile omits one, that means you definitely want the default value; the component class shouldn't use a value from Hiera data. If you need to set a class parameter that was omitted previously, refactor the profile.


In addition to these thoughts, one could also see the two profile classes being refactored into normal classes of a seperate module, which may help to see the implications of different design approaches.


In conclusion, the following questions arise:

Upvotes: 1

Views: 761

Answers (1)

John Bollinger
John Bollinger

Reputation: 180201

As the consul_client profile is includeed / requireed by the consul_server profile, the test failed with missing parameters for the consul_client profile class. This seems to be indicative of some general structural problem with this approach.

Not particularly.

Do understand that if you use an include-like declaration of a class that has required parameters, then those parameters will need to obtain values either via automatic data binding or via a previous resource-like declaration of the same class. That's by no means a "general structural problem", though, for in general, one ought to be feeding parameters to classes via Hiera (and thus automatic data binding) anyway.

That does become a little trickier in a unit testing context. It is possible to configure Hiera data for your test suite, but it's probably easier to write a precondition instead. I did something very similar just today, in fact. Example:

describe 'profile::consul_server' do
  # ...
  context "..." do
    let(:pre_condition) do
      'class { "profile::consul_client": param1 => "value1", param2 => "value2" }'
    end
    let(:params) { { bootstrap_expect: 3 } }

    it do
      is_expected.to # ...
    end
  end
end

HOWEVER, although there's no particular structural problem here, there is a probable semantic problem. If your consul_server profile includes your consul_client profile then that suggests that every consul server must also be a consul client. I'm not so familiar with consul, so I can't be certain that that's not sensible, but it's at least fishy. If servers are not necessarily clients then code that happens to be shared between the two profiles is only incidentally duplicate, and squeezing out such incidental duplication is more likely to cause problems than to solve them.

Now, I am unsure if I should re-declare all the parameters of the consul_client profile class in the consul_server profile class - which, in my opinion, would violate the DRY principle. Also, when using Hiera data in the future, this would lead to a situation where profile::consul_client::* and profile::consul_server::* would contain some of the same, duplicate data, as the client-related part of the data would have to be repeated for both profiles.

These are reasonable concerns. Personally, I minimize the number of parameters my profiles classes have. Most have none at all. My component classes are parameterized, and their parameter values mostly come from automated data binding. Only rarely do my profile classes use resource-like declarations of component classes, but when they do, it is almost always driven by the identity of the profile, not its parameters.

Additionally, you may find that it it would ease your problem to refactor. If there are configuration details shared between your consul servers and clients, then they probably would be suited to management via one or more component classes that both profiles declare. There is then no need for data duplication, because it is the parameters of those shared classes to which you would bind data.

Upvotes: 0

Related Questions