puppetrspec-puppet

Puppet 8: RSpec can't resolve default value declared in params file for another module


Context

We have added an extra unit test job to our CI to test our puppet 6 code on puppet 8 to get a first estimate of how much code refactoring would be involved.

Testing our code on puppet 8 with the following configuration (actual scenario detailed in the use case):

classA.pp

class moduleA::classA {
  moduleB::defineB { 'some_resource': }
}

defineB.pp

define moduleB::defineB (
  paramB = moduleB::params::default_paramB
) {}

now produces the following error:

Error: Unknown variable: 'moduleB::params::default_paramB'.

Questions

  1. How to properly handle default parameter values from included modules (declared in a params.pp class) when writing RSpec puppet unit tests ?
  2. Is there a way to avoid redeclaring a multitude of pre-conditions in all unit tests ?

Use case

Calling filebeat (version 4.14.0) from moduleA :

class moduleA::classA {

  filebeat::input { 'resource': }
}

filebeat input.pp manifest:

define filebeat::input (
  ...
  String $input_type    = $filebeat::params::default_input_type,
  ...
)

$default_input_type parameter declaration in filebeat params.pp:

    ...
    if versioncmp($facts['filebeat_version'], '7.10') > 0 {
      $default_input_type = 'filestream'
    } else {
      $default_input_type = 'log'
    }
  } else {
    $default_input_type = 'filestream'
  }

unit test

The following unit test works with puppet 6 / 7

require 'spec_helper'

describe 'moduleA::classA' do
  on_supported_os.each do |os, facts|
    context "On #{os} with default values for all parameters" do
      it { is_expected.to compile }
      it { is_expected.to contain_class('moduleA::classA') }
    end
  end
end

test execution from within moduleA directory:

bundle exec rake spec SPEC=spec/classes/classA_spec.rb

Error

Error: Unknown variable: 'filebeat::params::default_input_type'.

Discussion

I already tried a few ways of working around this but my main question is whether rspec is able to pick up default values (and if not how should we handle them) ?

For instance including filebeat's params.pp class as a pre-condition:

require 'spec_helper'

describe 'moduleA::classA' do
  on_supported_os.each do |os, facts|
    context "On #{os} with default values for all parameters" do
      let(:pre_condition) { 'include filebeat::params' }
      it { is_expected.to compile }
      it { is_expected.to contain_class('moduleA::classA') }
    end
  end
end

results in another error as it now seems that facts are not properly resolved in the context of filebeat's params.pp:

Operator '[]' is not applicable to an Undef Value.

which refers to the following line:

$osfamily_fail_message    = "${facts['os']['family']} is not supported by filebeat."

Edit (solution for this specific issue)

Going through the use case error by error :

puppet code

classA references filebeat::input:

class moduleA::classA {

  filebeat::input { 'resource': }
}
  1. Missing definition for variables referenced in filebeat::input and declared in filebeat::params:
Error: Unknown variable: 'filebeat::params::default_input_type'.

solution

Add filebeat::params as a pre-condition:

let(:pre_condition) do
  <<-EOF
    include filebeat::params
  EOF
end
  1. Missing definition for variables referenced in filebeat::input and declared in filebeat class:
Unknown variable: 'filebeat::fields_under_root'.

this variable is declared in filebeat's init.pp class (which is the manifest that declares filebeat class):

Boolean $fields_under_root = $filebeat::fields_under_root,

classA references filebeat::input which in turn needs the filebeat class to be already declared.

solution

Include the filebeat class in the :pre-condition block as well:

let(:pre_condition) do
  <<-EOF
    include filebeat::params
    include filebeat
  EOF
end
  1. Missing facts in filebeat::params:
Operator '[]' is not applicable to an Undef Value.

which refers to the following line in filebeat::params:

$osfamily_fail_message    = "${facts['os']['family']} is not supported by filebeat."

solution

As facts are already available via the rspec-puppet-facts framework I only needed make use of the :facts block properly:

on_supported_os.each do |os, facts|
  let(:facts) do
    facts.merge(
      filebeat_version: '7',
    )
  end

Solution

  • I already tried a few ways of working around this but my main question is whether rspec is able to pick up default values (and if not how should we handle them) ?

    RSpec itself knows nothing about Puppet at all.

    You appear to be using rspec-puppet to support RSpec testing of your Puppet classes, and this is where the Puppet knowledge comes in. Nevertheless, rspec-puppet does not know specifically about default values, and it doesn't need to do. It works by using Puppet itself to compile your manifests into a catalog, and by providing RSpec matchers for evaluating assertions about the resulting catalog's contents. Puppet certainly understands default parameter values.

    My best guess would be that this ...

    Error: Unknown variable: 'filebeat::params::default_input_type'.
    

    ... indicates that class filebeat::params has not (yet) been declared into the catalog at the point where the error is triggered. It is plausible that this was not an issue for your tests before Puppet 8 because starting in Puppet 8, strict mode is enabled by default. In earlier versions, the variable reference would, I think, have simply expanded to undef. Depending on how the type under test is intended to be used, this possibly highlights a weakness in the module.

    That ...

    including filebeat's params.pp class as a pre-condition [...] results in another error

    ... supports this hypothesis. It's unlikely that that is a new issue. You just didn't run into it before in your test suite because the filebeat::params class was not being declared at all.

    In particular ...

    facts are not properly resolved in the context of filebeat's params.pp

    ... seems unlikely. I would be inclined to think that your test suite simply does not provide the os fact. rspec-puppet has a mechanism for you to define default facts for your test cases, and one for you to define per-test facts, but it does not provide any facts by itself. For this purpose, you might be interested in the separate rspec-puppet-facts project.