Reputation: 11
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 include
ed / require
ed 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:
consul_client
profile class be the better option?Upvotes: 1
Views: 761
Reputation: 180201
As the
consul_client
profile isinclude
ed /require
ed by theconsul_server
profile, the test failed with missing parameters for theconsul_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 theconsul_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 whereprofile::consul_client::*
andprofile::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