diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml index d15d148..e028483 100644 --- a/.github/workflows/auto_release.yml +++ b/.github/workflows/auto_release.yml @@ -1,8 +1,6 @@ name: "Auto release" on: - schedule: - - cron: '0 3 * * 6' workflow_dispatch: env: diff --git a/.pdkignore b/.pdkignore index 33a1347..c538bea 100644 --- a/.pdkignore +++ b/.pdkignore @@ -27,6 +27,7 @@ /inventory.yaml /spec/fixtures/litmus_inventory.yaml /appveyor.yml +/.editorconfig /.fixtures.yml /Gemfile /.gitattributes diff --git a/.sync.yml b/.sync.yml index f405812..c3c4a29 100644 --- a/.sync.yml +++ b/.sync.yml @@ -10,9 +10,12 @@ Gemfile: optional: ':development': - gem: 'puppet-resource_api' - git: 'https://github.com/michaeltlombardi/puppet-resource_api' - branch: 'gh-225/main/custom-insync' - gem: 'github_changelog_generator' + - gem: 'ruby-pwsh' + - gem: 'webmock' + - gem: 'pry-byebug' + - gem: 'retriable' + version: '~> 3.1' spec/spec_helper.rb: mock_with: ':rspec' .gitlab-ci.yml: diff --git a/Gemfile b/Gemfile index 5176bd2..f0c024b 100644 --- a/Gemfile +++ b/Gemfile @@ -26,9 +26,10 @@ group :development do gem "puppet-module-win-dev-r#{minor_version}", '~> 1.0', require: false, platforms: [:mswin, :mingw, :x64_mingw] gem "puppet-resource_api", require: false gem "github_changelog_generator", require: false - gem 'retriable', '~> 3.1', require: false - gem 'pry-byebug', require: false - gem 'webmock', require: false + gem "ruby-pwsh", require: false + gem "webmock", require: false + gem "pry-byebug", require: false + gem "retriable", '~> 3.1', require: false end group :system_tests do gem "puppet-module-posix-system-r#{minor_version}", '~> 1.0', require: false, platforms: [:ruby] diff --git a/lib/puppet/provider/check_powershell/check_powershell.rb b/lib/puppet/provider/check_powershell/check_powershell.rb new file mode 100644 index 0000000..cc0b411 --- /dev/null +++ b/lib/puppet/provider/check_powershell/check_powershell.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' +require 'puppet/resource_api/simple_provider' +require 'ruby-pwsh' +require 'retriable' + +# Implementation for the check_powershell type using the Resource API. +class Puppet::Provider::CheckPowershell::CheckPowershell + def get(_context) + [] + end + + def set(context, changes); end + + # Update the check_powershell provider to use the above attributes to execute up to retries number of times + # with success being defined as having one of the expected_statuses + # and the body of the response matches body_matcher while taking into account request_timeout. + + def insync?(context, _name, attribute_name, _is_hash, should_hash) + context.debug("Checking whether #{attribute_name} is up-to-date") + + posh = Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args) + # This callback provides the exception that was raised in the current try, the try_number, the elapsed_time for all tries so far, and the time in seconds of the next_interval. + do_this_on_each_retry = proc do |exception, try, elapsed_time, next_interval| + context.info("#{exception.class}: '#{exception.message}' - #{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try.") unless exception.nil? + end + + Retriable.retriable(tries: should_hash[:retries], max_elapsed_time: should_hash[:request_timeout], max_interval: should_hash[:max_backoff], +multiplier: should_hash[:exponential_backoff_base], on_retry: do_this_on_each_retry) do + response = posh.execute(should_hash[:command]) + unless should_hash[:expected_exitcode].include? response[:exitcode].to_i + raise Puppet::Error, "check_powershell exitcode check failed. The return exitcode '#{response[:exitcode]}' is not matching with the expected_exitcode '#{should_hash[:expected_exitcode]}.to_s'" + end + context.debug("The return exitcode '#{response[:exitcode]}' is matching with the expected_exitcode '#{should_hash[:expected_exitcode]}'") + unless response[:stdout].match(should_hash[:output_matcher]) + raise Puppet::Error, "check_powershell output check failed. The return output '#{response[:stdout]}' is not matching output_matcher '#{should_hash[:output_matcher]}'" + end + context.debug("The return output '#{response[:stdout]}' is matching with output_matcher '#{should_hash[:output_matcher]}'") + context.debug("Successfully executed the command '#{should_hash[:command]}'") + return true + end + false + end +end diff --git a/lib/puppet/type/check_powershell.rb b/lib/puppet/type/check_powershell.rb new file mode 100644 index 0000000..7f1bb4a --- /dev/null +++ b/lib/puppet/type/check_powershell.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'check_powershell', + docs: <<-EOS, +@summary a check_powershell type +@example +check_powershell { 'https://www.example.com': } + +Use this to check whether a web server is responding correctly. This can be used both as a prerequisite (don't manage something if a dependency is unhealthy) or to check whether everything went right after managing something. +EOS + features: ['custom_insync'], + attributes: { + command: { + type: 'String', + desc: 'The powershell command to run.', + behaviour: :namevar, + }, + expected_exitcode: { + type: 'Array[Integer]', + desc: 'An array of acceptable exit codes.', + behaviour: :parameter, + default: [0], + }, + output_matcher: { + type: 'Regexp', + desc: 'A call is considered a success if its output matches this regular expression', + behaviour: :parameter, + default: //, + }, + execution_timeout: { + type: 'Numeric', + desc: 'Number of seconds for a single execution to wait for a response to return a success before aborting.', + behaviour: :parameter, + default: 60, + }, + retries: { + type: 'Integer', + desc: 'Number of requests to make before giving up.', + behaviour: :parameter, + default: 1, + }, + backoff: { + type: 'Numeric', + desc: 'Initial number of seconds to wait between requests.', + behaviour: :parameter, + default: 10, + }, + exponential_backoff_base: { + type: 'Numeric', + desc: 'Exponential base for the exponential backoff calculations.', + behaviour: :parameter, + default: 2, + }, + max_backoff: { + type: 'Numeric', + desc: 'An upper limit to the backoff duration.', + behaviour: :parameter, + default: 120, + }, + timeout: { + type: 'Numeric', + desc: 'Number of seconds allocated overall for the check to return a success before giving up.', + behaviour: :parameter, + default: 600, + }, + }, +) diff --git a/metadata.json b/metadata.json index ebfd0f8..819f80e 100644 --- a/metadata.json +++ b/metadata.json @@ -61,7 +61,7 @@ "version_requirement": ">= 6.21.0 < 8.0.0" } ], - "pdk-version": "2.1.0 (1)", + "pdk-version": "2.1.0", "template-url": "https://github.com/puppetlabs/pdk-templates#main", - "template-ref": "heads/main-0-g0a06ce2" + "template-ref": "tags/2.1.1-0-g03daa92" } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b367fde..9b1fa6f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -47,6 +47,18 @@ c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT'] c.after(:suite) do end + + # Filter backtrace noise + backtrace_exclusion_patterns = [ + %r{spec_helper}, + %r{gems}, + ] + + if c.respond_to?(:backtrace_exclusion_patterns) + c.backtrace_exclusion_patterns = backtrace_exclusion_patterns + elsif c.respond_to?(:backtrace_clean_patterns) + c.backtrace_clean_patterns = backtrace_exclusion_patterns + end end # Ensures that a module is defined diff --git a/spec/unit/puppet/provider/check_powershell/check_powershell_spec.rb b/spec/unit/puppet/provider/check_powershell/check_powershell_spec.rb new file mode 100644 index 0000000..0232dea --- /dev/null +++ b/spec/unit/puppet/provider/check_powershell/check_powershell_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::CheckPowershell') +require 'puppet/provider/check_powershell/check_powershell' + +RSpec.describe Puppet::Provider::CheckPowershell::CheckPowershell do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext') } + let(:posh) { instance_double('Pwsh::Manager') } + let(:valid_command) { '$PSVersionTable.PSVersion' } + let(:invalid_command) { 'invalid$PSVersion' } + let(:valid_hash) do + { name: 'foo', command: valid_command, expected_exitcode: [0], output_matcher: %r{Major}, request_timeout: 30, retries: 1, backoff: 1, exponential_backoff_base: 2, max_backoff: 40, timeout: 60 } + end + let(:invalid_hash) do + { name: 'foos', command: invalid_command, expected_exitcode: [2], output_matcher: %r{test}, request_timeout: 30, retries: 3, backoff: 1, exponential_backoff_base: 2, max_backoff: 40, timeout: 60 } + end + + describe 'get(context)' do + it 'processes resources' do + expect(provider.get(context)).to eq [] + end + end + + describe 'insync?(context, name, attribute_name, is_hash, should_hash) without Retry' do + it 'processes resources' do + allow(Pwsh::Manager).to receive(:powershell_path).and_return('C:\\Windows') + allow(Pwsh::Manager).to receive(:powershell_args).and_return(['-NoProfile']) + allow(Pwsh::Manager).to receive(:instance).with(any_args).and_return(posh) + allow(posh).to receive(:execute).with(valid_command).and_return({ stdout: 'Major', exitcode: 0 }) + expect(context).to receive(:debug).with('Checking whether foo is up-to-date') + expect(context).to receive(:debug).with("The return exitcode '0' is matching with the expected_exitcode '[0]'") + expect(context).to receive(:debug).with("The return output 'Major' is matching with output_matcher '(?-mix:Major)'") + expect(context).to receive(:debug).with("Successfully executed the command '$PSVersionTable.PSVersion'") + expect(provider.insync?(context, 'foo', 'foo', valid_hash, valid_hash)).to be(true) + end + end + + describe 'insync?(context, name, attribute_name, is_hash, should_hash) expected_exitcode not matching' do + it 'processes resources' do + allow(Pwsh::Manager).to receive(:powershell_path).and_return('C:\\Windows') + allow(Pwsh::Manager).to receive(:powershell_args).and_return(['-NoProfile']) + allow(Pwsh::Manager).to receive(:instance).with(any_args).and_return(posh) + allow(posh).to receive(:execute).with(invalid_command).and_return({ stdout: 'Major', exitcode: 3 }) + allow(context).to receive(:debug) + allow(context).to receive(:debug) + expect(context).to receive(:debug).with('Checking whether foo is up-to-date') + expect(context).to receive(:info).with(%r{1 tries}) + expect(context).to receive(:info).with(%r{2 tries}) + expect(context).to receive(:info).with(%r{3 tries}) + expect { provider.insync?(context, 'foo', 'foo', invalid_hash, invalid_hash) }.to raise_error(%r{check_powershell exitcode check failed.}) + end + end + + describe 'insync?(context, name, attribute_name, is_hash, should_hash) output_matcher not matching' do + it 'processes resources' do + allow(Pwsh::Manager).to receive(:powershell_path).and_return('C:\\Windows') + allow(Pwsh::Manager).to receive(:powershell_args).and_return(['-NoProfile']) + allow(Pwsh::Manager).to receive(:instance).with(any_args).and_return(posh) + allow(posh).to receive(:execute).with(invalid_command).and_return({ stdout: 'invalid', exitcode: 2 }) + allow(context).to receive(:debug) + allow(context).to receive(:debug) + expect(context).to receive(:debug).with('Checking whether foo is up-to-date') + expect(context).to receive(:debug).with("The return exitcode '2' is matching with the expected_exitcode '[2]'") + expect(context).to receive(:info).with(%r{1 tries}) + expect(context).to receive(:info).with(%r{2 tries}) + expect(context).to receive(:info).with(%r{3 tries}) + expect { provider.insync?(context, 'foo', 'foo', invalid_hash, invalid_hash) }.to raise_error(%r{check_powershell output check failed.}) + end + end + + describe 'insync?(context, name, attribute_name, is_hash, should_hash) with Retry' do + it 'processes resources' do + allow(Pwsh::Manager).to receive(:powershell_path).and_return('C:\\Windows') + allow(Pwsh::Manager).to receive(:powershell_args).and_return(['-NoProfile']) + allow(Pwsh::Manager).to receive(:instance).with(any_args).and_return(posh) + allow(context).to receive(:debug) + allow(context).to receive(:debug) + expect(context).to receive(:debug).with('Checking whether foo is up-to-date') + allow(posh).to receive(:execute).with(invalid_command).and_raise(StandardError) + expect(context).to receive(:info).with(%r{1 tries}) + expect(context).to receive(:info).with(%r{2 tries}) + expect(context).to receive(:info).with(%r{3 tries}) + expect { provider.insync?(context, 'foo', 'foo', invalid_hash, invalid_hash) }.to raise_error(StandardError) + end + end +end diff --git a/spec/unit/puppet/type/check_powershell_spec.rb b/spec/unit/puppet/type/check_powershell_spec.rb new file mode 100644 index 0000000..920b663 --- /dev/null +++ b/spec/unit/puppet/type/check_powershell_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/check_powershell' + +RSpec.describe 'the check_powershell type' do + it 'loads' do + expect(Puppet::Type.type(:check_powershell)).not_to be_nil + end +end